react自動化構建路由的實現
序
在使用react-router-dom在編寫專案的時候有種感覺就是,使用起來非常的方便,但是若是維護起來,那便是比較麻煩了,因為各大路由分散在各個元件中. 所以我們就會想到,使用react-router-dom中提供的config模式來編寫我們的路由,這樣寫的好處就是我們可以將邏輯集中在一處,配置路由比較方便
專案地址
https://gitee.com/d718781500/autoRouter
1.路由集中式
我們先將下列資料定義在/src/router/index.js中
在react的路由官方文件中就提供了配置集中式路由的案例,大致是這樣的仿照vue的路由,生成一個配置檔案,預期是這樣的
//需要一個路由的配置,它是一個數組 import Discover from "../pages/Discover" import Djradio from "../pages/Discover/Djradio" import Playlist from "../pages/Discover/Playlist" import Toplist from "../pages/Discover/Toplist" import Friends from "../pages/Friends" import Mine from "../pages/Mine" import Page404 from "../pages/Page404" const routes = [ { path: "/friends",component: Friends },{ path: "/mine",component: Mine },{ path: "/discover",component: Discover,children: [ { path: "/discover/djradio",component: Djradio },{ path: "/discover/playlist",component: Playlist },{ path: "/discover/toplist",component: Toplist } ] },{//Page404這個配置一定要在所有路由配置之後 path: "*",component: Page404 } ] export default routes
我們可以通過上述配置,來生成一個路由.當然上述的配置也只是做了簡單的處理,還有redirect exact等屬性沒有寫,我們還是從一個簡單的開始吧
2.檔案目錄
上述的配置中使用了類似於vue的集中式路由配置模式,那麼下面就展示下我當前這個demo的結構目錄吧
專案目錄結構
src/pages目錄結構
├─Discover │ │ abc.js │ │ index.js │ │ │ ├─Djradio │ │ │ index.js │ │ │ lf.js │ │ │ │ │ └─gv │ │ index.js │ │ │ ├─Playlist │ │ index.js │ │ │ └─Toplist │ index.js │ ├─Entertaiment │ index.js │ ├─Friends │ index.js │ xb.js │ ├─Mine │ index.js │ └─Page404 index.js
有了這些結構之後,那麼在1中提到的引入檔案結合起來看就不懵逼啦,接下來我們可以封裝一個元件,給他取個名字叫做CompileRouter這個元件專門用於編譯路由
3.建立CompileRouter
這個元件我們把它建立在src/utils中,作用就是通過傳入的路由配置,然後計算出這個元件,那麼問題來了,為什麼要建立這個元件呢?
讓我們回顧一下react路由的編寫方式吧,react路由需要一個基礎元件HashRouter或者BrowserRouter這兩個相當於一個基石元件
然後還需要一個路由配方這個元件可以接受一個path對映一個component
我們來寫段虛擬碼來說明一下
//引入路由基本元件(要在專案中安裝 npm i react-router-dom) import {HashRouter as Router,Route} from "react-router-dom" class Demo extends React.Component { render(){ //基石路由 <Router> //路由配方元件 通過path匹配component <Route path="/" component={Home}/> <Route path="/mine" component={Mine}/> </Router> } }
這是基本用法,所以我們CompileRouter這個元件的工作就是,生成如上程式碼中的Route一樣,生成Route然後展示在元件上
在瞭解到Compile的基本作用之後,下面我們就開始編碼吧
我個CompileRouter設計是接受一個數據,這個資料必須是符合路由配置的一個數組,就像1裡程式碼中所示的陣列一樣,接受的屬性為routes
//這個檔案通過routes配置來編譯出路由 import React from 'react' import { Switch,Route } from "react-router-dom"; export default class CompileRouter extends React.Component { constructor() { super() this.state = { c: [] } } renderRoute() { let { routes } = this.props;//獲取routes路由配置 //1.通過routes生成Route元件 //確保routes是一個數組 // console.log(routes) //render 不會重複讓元件的componentDidMount和componentWillUnmount重複呼叫 if (Array.isArray(routes) && routes.length > 0) { //確保傳入的routes是個陣列 // 迴圈迭代傳入的routes let finalRoutes = routes.map(route => { //每個route是這個樣子的 {path:"xxx",component:"xxx"} //如果route有子節點 {path:"xxx",component:"xxx",children:[{path:"xxx"}]} return <Route path={route.path} key={route.path} render={ // 這麼寫的作用就是,如果路由還有巢狀路由,那麼我們可以把route中的children中的配置資料傳遞給這個元件,讓元件再次呼叫CompileRouter的時候就能編譯出巢狀路由了 () => <route.component routes={route.children} /> } /> }) this.setState({ c: finalRoutes }) } else { throw new Error('routes必須是一個數組,並且長度要大於0') } } componentDidMount() { //確保首次呼叫renderRoute計算出Route元件 this.renderRoute() } render() { let { c } = this.state; return ( <Switch> {c} </Switch> ) } }
上述程式碼就是用於去處理routes資料並且聲稱這樣的元件,每一步的作用我都已經在上面用註釋標明瞭
4.使用CompileRouter
其實我們可以把封裝的這個元件當成是vue-router中的檢視元件<router-view/>就暫且先這麼認為吧,接下來我們需要在頁面上渲染1級路由了
在src/app.js
import React from 'react' import { HashRouter as Router,Link } from 'react-router-dom' //引入我們封裝的CompileRouter罪案 import CompileRouter from "./utils/compileRouter" //引入在1中定義的路由配置資料 import routes from "./router" console.log(routes) class App extends React.Component { render() { return ( <Router> <Link to="/friends">朋友</Link> | <Link to="/discover">發現</Link> | <Link to="/mine">我的</Link> {/*當成是vue-router的檢視元件 我們需要將路由配置資料傳入*/} <CompileRouter routes={routes} /> </Router> ) } } export default App
寫完後,那麼頁面上其實就可以完美的展示1級路由了
5.巢狀路由處理
上面我們已經對1級路由進行了渲染,可以跳轉,但是二級路由怎麼處理呢?其實也很簡單,我們只需要找到二級路由的父路由,繼續使用CompileRouter就可以了
我們從配置中可以看到,Discover這個路由是具有巢狀路由的,所以我們就以Discover路由為例子,首先我們看下結構圖
圖上的index.js就是Discover這個檢視元件了,也是巢狀路由的父級路由,所以我們只需要在這個index.js中繼續使用CompileRouter就可以了
import React from 'react' import { Link } from "react-router-dom" import CompileRouter from "../../utils/compileRouter" function Discover(props) { let { routes } = props //這個資料是從ComileRouter元件編譯的時候傳遞過來的children // console.log(rouhttp://www.cppcns.comtes) let links = routes.map(route => { return ( <li key={route.path}> <Link to={route.path}>{route.path}</Link> </li> ) }) return ( <fieldset> <legend>發現</legend> <h1>我發現,不能說多喝熱水</h1> <ul> {links} </ul> {/*核心程式碼,再次使用即可 這裡將通過children資料可以渲染出Route*/} <CompileRouter routes={routes} /> </fieldset> ) } Discover.meta = { title: "發現",icon: "" } export default Discover
所以我們以後記住,只要是有巢狀路由我們要做兩件事
- 配置routes
- 在巢狀路由的父級路由中再次使用CompileRouter,並且傳入routes即可
6. require.context
上面我們實現了一個路由集中式的配置,但是我們會發現一個問題
引入了很多的元件,實際上,在專案中引入的更多,如果一個一個引入,對我們來說是災難性的,所以我們可以使用webpack提供的一個很好用的api,require.context我們先說說它是怎麼使用的吧
自動化匯入require.context方法,使用這個方法可以減少繁瑣的元件引入,而且可以深度的遞迴目錄,做到import做不到的事情 下面我們來看一下這個方法是如何使用的
使用
你可以通過 require.context() 函式來建立自己的 context。
可以給這個函式傳入4個引數:
- 一個要搜尋的目錄,
- 一個標記表示是否還要搜尋其子目錄,
- 一個匹配檔案的正則表示式。
- mode 模組載入模式,常用值為 syncgZRAX、lazy、lazy-once、eager
- sync 直接打包到當前檔案,同步載入並執行
- lazy 延遲載入會分離出單獨的 chunk 檔案
- lazy-once 延遲載入會分離出單獨的 chunk 檔案,載入過下次再載入直接讀取記憶體裡的程式碼。
- eager 不會分離出單獨的 chunk 檔案,但是會返回 promise,只有呼叫了 promise 才會執行程式碼,可以理解為先載入了程式碼,但是我們可以控制延遲執行這部分程式碼。
webpack 會在構建中解析程式碼中的 require.context() 。
語法如下:
require.context( directory,(useSubdirectories = true),(regExp = /^\.\/.*$/),(mode = 'sync') );
示例:
require.context('./test',false,/\.test\.js$/); //(創建出)一個 context,其中檔案來自 test 目錄,request 以 `.test.js` 結尾。 require.context('../',true,/\.stories\.js$/); // (創建出)一個 context,其中所有檔案都來自父資料夾及其所有子級資料夾,request 以 `.stories.js` 結尾。
api
函式有三個屬性:resolve,keys,id。
resolve 是一個函式,它返回 request 被解析後得到的模組 id。
let p = require.context("...","xxx") p.resolve("一個路徑")
keys 也是一個函式,它返回一個數組,由所有可能被此 context module 處理的請求(譯者注:參考下面第二段程式碼中的 key)組成。
require.context的返回值是一個函式,我們可以在函式中傳入檔案的路徑,就可以得到模組化的元件了
let components = require.context('../pages',/\.js$/,'sync') let paths = components.keys()//獲得了所有引入檔案的地址 // console.log(paths) let routes = paths.map(path => { let component = components(path).default path = path.substr(1).replace(/\/\w+\.js$/,"") return { path,component } }) console.log(routes)
總結
雖然上面有很多api和返回的值,我們只拿兩個來做說明
keys方法,這個可以獲取所有模組的路徑,返回的是一個數組
let context = require.context("../pages",/\.js$/); let paths = context.keys()//獲取了所有檔案的路徑
獲取路徑下所有的模組
let context = require.context("../pages",/\.js$/); let paths = context.keys()//獲取了所有檔案的路徑 let routes = paths.map(path => { //批量獲取引入的元件 let component = context(path).default; console.log(component) })
掌握這兩個就可以了,下面我們來繼續處理
7.扁平資料轉換為樹形結構的(convertTree演算法)
這個演算法的名字是我自己起的,首先我們要明白為甚麼需要將資料轉換成tree
我們的預期的routes資料應該是下面這樣的
//目的是什麼? //生成一個路由配置 const routes = [ { path: "",component:xxx children:[ { path:"xxx" component:xxx } ] } ]
但其實我們使用require.context處理之後的資料是這樣的
可以看到這個資料是完全扁平化的,沒有任何的巢狀,所以我們第一步就是要實現將這種扁平化的資料轉換為符合我們預期的樹形結構,下面我們一步一步來
7.1使用require.context將資料處理成扁平化
首先要處理成上圖那樣的結構,程式碼都有註釋,難度也不高
//require.context() // 1. 一個要搜尋的目錄, // 2. 一個標記表示是否還要搜尋其子目錄, // 3. 一個匹配檔案的正則表示式。 let context = require.context("../pages",/\.js$/); let paths = context.keys()//獲取了所有檔案的路徑 let routes = paths.map(path => { //批量獲取引入的元件 let component = context(path).default; //元件擴充套件屬性方便渲染選單 let meta = component['meta'] || {} //console.log(path) //這個正則的目的 //因為地址是./Discover/Djradio/index.js這種型別的並不能直接使用,所以要進行處理 //1.接去掉最前的"." 得到的結果是/Discover/Djradio/index.js //2.處理了還是不能直接用 因為我們的預期/Discover/Djradio,所以通過正則將index.js幹掉了 //3.有可能後面的路徑不是資料夾 得到的結果是/Discover/abc.js,字尾名並不能用到路由配置的path屬性中,所以.js字尾名又用正則替換掉 path = path.substr(1).replace(/(\/index\.js|\.js)$/,"") // console.log(path) return { path,component,meta } })
7.2 實現convertTree演算法
上面處理好了資料後,我們封裝一個方法,專門用於處理扁平化資料變成樹形資料,演算法時間複雜度為O(n^2)
function convertTree(routes) { let treeArr = []; //1.處理資料 將每條資料的id和parent處理好 (俗稱 爸爸去哪兒了) routes.forEach(route => { let comparePaths = route.path.substr(1).split("/") // console.log(comparePaths) if (comparePaths.length === 1) { //說明是根節點,根節點不需要新增parent_id route.id = comparePaths.join("") } else { //說明具有父節點 //先處理自己的id route.id = comparePaths.join(""); //comparePaths除去最後一項就是parent_id comparePaths.pop() route.parent_id = comparePaths.join("") } }) //2.所有的資料都已經找到了父節點的id,下面才是真正的找父節點了 routes.forEach(route => { //判斷當前的route有沒有parent_id if (route.parent_id) { //有父節點 //id===parent_id的那個route就是當前route的父節點 程式設計客棧 let target = routes.find(v => v.id === route.paren程式設計客棧t_id); //判斷父節點有沒有children這個屬性 if (!target.children) { target.children = [] } target.children.push(route) } else { treeArr.push(route) } }) return treeArr }
通過上述處理之後就可以得到樹形結構啦
接下來我們只需要把資料匯出去,在app上引入傳遞給CompileRouter元件就可以了
7.3 以後要注意的
以後只需要在pages中建立檔案即可自動實現路由的處理以及編譯了,不過對於巢狀級別的路由咱們別忘了要在路由元件加上CompileRouter元件,總結為亮點
- 建立路由頁面
- 巢狀路由的父級路由元件中加入
8.擴充套件靜態屬性
我們當前創建出來的效果是有了,但是如果我們用於渲染選單的時候就會有問題,沒有內容可以用於渲染選單,所以我們可以給元件上擴充套件靜態屬性meta(也可以是別的),然後對我們的自動化編譯程式碼做一些小小的改動就行了
元件
自動化處理邏輯完整程式碼
//require.context() // 1. 一個要搜尋的目錄, // 2. 一個標記表示是否還要搜尋其子目錄, // 3. 一個匹配檔案的正則表示式。 let context = require.context("../pages",www.cppcns.commeta } }) //這種資料是扁平化的資料,並不符合我們的路由規則 //需要做演算法 儘可能將時間複雜度降低o(n)最好 //封裝一個convertTree演算法 時間複雜度o(n^2) // console.log(routes) //id //parent_id function convertTree(routes) { let treeArr = []; //1.處理資料 將每條資料的id和parent處理好 (俗稱 爸爸去哪兒了) routes.forEach(route => { let comparePaths = route.path.substr(1).split("/") // console.log(comparePaths) if (comparePaths.length === 1) { //說明是根節點,下面才是真正的找父節點了 routes.forEach(route => { //判斷當前的route有沒有parent_id if (route.parent_id) { //有父節點 //id===parent_id的那個route就是當前route的父節點 let target = routes.find(v => v.id === route.parent_id); //判斷父節點有沒有children這個屬性 if (!target.children) { target.children = [] } target.children.push(route) } else { treeArr.push(route) } }) return treeArr } export default convertTree(routes) //獲取一個模組 // console.log(p("./Discover/index.js").default) //目的是什麼? //生成一個路由配置 // const routes = [ // { // path: "",// component,// children:[ // {path component} // ] // } // ]
寫在最後
其實上述的處理並不能作為應用級別用於專案中,主要在於CompileRouter處理的不夠細緻,下一期我將專門寫一篇如何處理CompileRouter用於鑑權等應用在專案中
到此這篇關於react自動化構建路由的實現的文章就介紹到這了,更多相關react自動化構建路由內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!