序
在使用react-router-dom在編寫項(xiàng)目的時(shí)候有種感覺(jué)就是,使用起來(lái)非常的方便,但是若是維護(hù)起來(lái),那便是比較麻煩了,因?yàn)楦鞔舐酚煞稚⒃诟鱾€(gè)組件中. 所以我們就會(huì)想到,使用react-router-dom中提供的config模式來(lái)編寫我們的路由,這樣寫的好處就是我們可以將邏輯集中在一處,配置路由比較方便
項(xiàng)目地址
https://gitee.com/d718781500/autoRouter
1.路由集中式
我們先將下列數(shù)據(jù)定義在/src/router/index.js中
在react的路由官方文檔中就提供了配置集中式路由的案例,大致是這樣的仿照vue的路由,生成一個(gè)配置文件,預(yù)期是這樣的
//需要一個(gè)路由的配置,它是一個(gè)數(shù)組 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這個(gè)配置一定要在所有路由配置之后 path: "*", component: Page404 } ] export default routes
我們可以通過(guò)上述配置,來(lái)生成一個(gè)路由.當(dāng)然上述的配置也只是做了簡(jiǎn)單的處理,還有redirect exact等屬性沒(méi)有寫,我們還是從一個(gè)簡(jiǎn)單的開(kāi)始吧
2.文件目錄
上述的配置中使用了類似于vue的集中式路由配置模式,那么下面就展示下我當(dāng)前這個(gè)demo的結(jié)構(gòu)目錄吧
項(xiàng)目目錄結(jié)構(gòu)
src/pages目錄結(jié)構(gòu)
├─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
有了這些結(jié)構(gòu)之后,那么在1中提到的引入文件結(jié)合起來(lái)看就不懵逼啦,接下來(lái)我們可以封裝一個(gè)組件,給他取個(gè)名字叫做CompileRouter這個(gè)組件專門用于編譯路由
3.創(chuàng)建CompileRouter
這個(gè)組件我們把它創(chuàng)建在src/utils中,作用就是通過(guò)傳入的路由配置,然后計(jì)算出這個(gè)組件,那么問(wèn)題來(lái)了,為什么要?jiǎng)?chuàng)建這個(gè)組件呢?
讓我們回顧一下react路由的編寫方式吧,react路由需要一個(gè)基礎(chǔ)組件HashRouter或者BrowserRouter這兩個(gè)相當(dāng)于一個(gè)基石組件
然后還需要一個(gè)路由配方這個(gè)組件可以接受一個(gè)path映射一個(gè)component
我們來(lái)寫段偽代碼來(lái)說(shuō)明一下
//引入路由基本組件(要在項(xiàng)目中安裝 npm i react-router-dom) import {HashRouter as Router,Route} from "react-router-dom" class Demo extends React.Component { render(){ //基石路由 <Router> //路由配方組件 通過(guò)path匹配component <Route path="/" component={Home}/> <Route path="/mine" component={Mine}/> </Router> } }
這是基本用法,所以我們CompileRouter這個(gè)組件的工作就是,生成如上代碼中的Route一樣,生成Route然后展示在組件上
在了解到Compile的基本作用之后,下面我們就開(kāi)始編碼吧
我個(gè)CompileRouter設(shè)計(jì)是接受一個(gè)數(shù)據(jù),這個(gè)數(shù)據(jù)必須是符合路由配置的一個(gè)數(shù)組,就像1里代碼中所示的數(shù)組一樣,接受的屬性為routes
//這個(gè)文件通過(guò)routes配置來(lái)編譯出路由 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.通過(guò)routes生成Route組件 //確保routes是一個(gè)數(shù)組 // console.log(routes) //render 不會(huì)重復(fù)讓組件的componentDidMount和componentWillUnmount重復(fù)調(diào)用 if (Array.isArray(routes) && routes.length > 0) { //確保傳入的routes是個(gè)數(shù)組 // 循環(huán)迭代傳入的routes let finalRoutes = routes.map(route => { //每個(gè)route是這個(gè)樣子的 {path:"xxx",component:"xxx"} //如果route有子節(jié)點(diǎn) {path:"xxx",component:"xxx",children:[{path:"xxx"}]} return <Route path={route.path} key={route.path} render={ // 這么寫的作用就是,如果路由還有嵌套路由,那么我們可以把route中的children中的配置數(shù)據(jù)傳遞給這個(gè)組件,讓組件再次調(diào)用CompileRouter的時(shí)候就能編譯出嵌套路由了 () => <route.component routes={route.children} /> } /> }) this.setState({ c: finalRoutes }) } else { throw new Error("routes必須是一個(gè)數(shù)組,并且長(zhǎng)度要大于0") } } componentDidMount() { //確保首次調(diào)用renderRoute計(jì)算出Route組件 this.renderRoute() } render() { let { c } = this.state; return ( <Switch> {c} </Switch> ) } }
上述代碼就是用于去處理routes數(shù)據(jù)并且聲稱這樣的組件,每一步的作用我都已經(jīng)在上面用注釋標(biāo)明了
4.使用CompileRouter
其實(shí)我們可以把封裝的這個(gè)組件當(dāng)成是vue-router中的視圖組件<router-view/>就暫且先這么認(rèn)為吧,接下來(lái)我們需要在頁(yè)面上渲染1級(jí)路由了
在src/app.js
import React from "react" import { HashRouter as Router, Link } from "react-router-dom" //引入我們封裝的CompileRouter罪案 import CompileRouter from "./utils/compileRouter" //引入在1中定義的路由配置數(shù)據(jù) import routes from "./router" console.log(routes) class App extends React.Component { render() { return ( <Router> <Link to="/friends">朋友</Link> | <Link to="/discover">發(fā)現(xiàn)</Link> | <Link to="/mine">我的</Link> {/*當(dāng)成是vue-router的視圖組件 我們需要將路由配置數(shù)據(jù)傳入*/} <CompileRouter routes={routes} /> </Router> ) } } export default App
寫完后,那么頁(yè)面上其實(shí)就可以完美的展示1級(jí)路由了
5.嵌套路由處理
上面我們已經(jīng)對(duì)1級(jí)路由進(jìn)行了渲染,可以跳轉(zhuǎn),但是二級(jí)路由怎么處理呢?其實(shí)也很簡(jiǎn)單,我們只需要找到二級(jí)路由的父路由,繼續(xù)使用CompileRouter就可以了
我們從配置中可以看到,Discover這個(gè)路由是具有嵌套路由的,所以我們就以Discover路由為例子,首先我們看下結(jié)構(gòu)圖
圖上的index.js就是Discover這個(gè)視圖組件了,也是嵌套路由的父級(jí)路由,所以我們只需要在這個(gè)index.js中繼續(xù)使用CompileRouter就可以了
import React from "react" import { Link } from "react-router-dom" import CompileRouter from "../../utils/compileRouter" function Discover(props) { let { routes } = props //這個(gè)數(shù)據(jù)是從ComileRouter組件編譯的時(shí)候傳遞過(guò)來(lái)的children // console.log(routes) let links = routes.map(route => { return ( <li key={route.path}> <Link to={route.path}>{route.path}</Link> </li> ) }) return ( <fieldset> <legend>發(fā)現(xiàn)</legend> <h1>我發(fā)現(xiàn),不能說(shuō)多喝熱水</h1> <ul> {links} </ul> {/*核心代碼,再次使用即可 這里將通過(guò)children數(shù)據(jù)可以渲染出Route*/} <CompileRouter routes={routes} /> </fieldset> ) } Discover.meta = { title: "發(fā)現(xiàn)", icon: "" } export default Discover
所以我們以后記住,只要是有嵌套路由我們要做兩件事
- 配置routes
- 在嵌套路由的父級(jí)路由中再次使用CompileRouter,并且傳入routes即可
6. require.context
上面我們實(shí)現(xiàn)了一個(gè)路由集中式的配置,但是我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題
引入了很多的組件,實(shí)際上,在項(xiàng)目中引入的更多,如果一個(gè)一個(gè)引入,對(duì)我們來(lái)說(shuō)是災(zāi)難性的,所以我們可以使用webpack提供的一個(gè)很好用的api,require.context我們先說(shuō)說(shuō)它是怎么使用的吧
自動(dòng)化導(dǎo)入require.context方法,使用這個(gè)方法可以減少繁瑣的組件引入,而且可以深度的遞歸目錄,做到import做不到的事情 下面我們來(lái)看一下這個(gè)方法是如何使用的
使用
你可以通過(guò) require.context() 函數(shù)來(lái)創(chuàng)建自己的 context。
可以給這個(gè)函數(shù)傳入4個(gè)參數(shù):
- 一個(gè)要搜索的目錄,
- 一個(gè)標(biāo)記表示是否還要搜索其子目錄,
- 一個(gè)匹配文件的正則表達(dá)式。
-
mode 模塊加載模式,常用值為 sync、lazy、lazy-once、eager
- sync 直接打包到當(dāng)前文件,同步加載并執(zhí)行
- lazy 延遲加載會(huì)分離出單獨(dú)的 chunk 文件
- lazy-once 延遲加載會(huì)分離出單獨(dú)的 chunk 文件,加載過(guò)下次再加載直接讀取內(nèi)存里的代碼。
- eager 不會(huì)分離出單獨(dú)的 chunk 文件,但是會(huì)返回 promise,只有調(diào)用了 promise 才會(huì)執(zhí)行代碼,可以理解為先加載了代碼,但是我們可以控制延遲執(zhí)行這部分代碼。
webpack 會(huì)在構(gòu)建中解析代碼中的 require.context() 。
語(yǔ)法如下:
require.context( directory, (useSubdirectories = true), (regExp = /^./.*$/), (mode = "sync") );
示例:
require.context("./test", false, /.test.js$/); //(創(chuàng)建出)一個(gè) context,其中文件來(lái)自 test 目錄,request 以 `.test.js` 結(jié)尾。 require.context("../", true, /.stories.js$/); // (創(chuàng)建出)一個(gè) context,其中所有文件都來(lái)自父文件夾及其所有子級(jí)文件夾,request 以 `.stories.js` 結(jié)尾。
api
函數(shù)有三個(gè)屬性:resolve, keys, id。
resolve 是一個(gè)函數(shù),它返回 request 被解析后得到的模塊 id。
let p = require.context("...",true,"xxx") p.resolve("一個(gè)路徑")
keys 也是一個(gè)函數(shù),它返回一個(gè)數(shù)組,由所有可能被此 context module 處理的請(qǐng)求(譯者注:參考下面第二段代碼中的 key)組成。
require.context的返回值是一個(gè)函數(shù),我們可以在函數(shù)中傳入文件的路徑,就可以得到模塊化的組件了
let components = require.context("../pages", true, /.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)
總結(jié)
雖然上面有很多api和返回的值,我們只拿兩個(gè)來(lái)做說(shuō)明
keys方法,這個(gè)可以獲取所有模塊的路徑,返回的是一個(gè)數(shù)組
let context = require.context("../pages", true, /.js$/); let paths = context.keys()//獲取了所有文件的路徑
獲取路徑下所有的模塊
let context = require.context("../pages", true, /.js$/); let paths = context.keys()//獲取了所有文件的路徑 let routes = paths.map(path => { //批量獲取引入的組件 let component = context(path).default; console.log(component) })
掌握這兩個(gè)就可以了,下面我們來(lái)繼續(xù)處理
7.扁平數(shù)據(jù)轉(zhuǎn)換為樹(shù)形結(jié)構(gòu)的(convertTree算法)
這個(gè)算法的名字是我自己起的,首先我們要明白為甚么需要將數(shù)據(jù)轉(zhuǎn)換成tree
我們的預(yù)期的routes數(shù)據(jù)應(yīng)該是下面這樣的
//目的是什么? //生成一個(gè)路由配置 const routes = [ { path: "", component:xxx children:[ { path:"xxx" component:xxx } ] } ]
但其實(shí)我們使用require.context處理之后的數(shù)據(jù)是這樣的
可以看到這個(gè)數(shù)據(jù)是完全扁平化的,沒(méi)有任何的嵌套,所以我們第一步就是要實(shí)現(xiàn)將這種扁平化的數(shù)據(jù)轉(zhuǎn)換為符合我們預(yù)期的樹(shù)形結(jié)構(gòu),下面我們一步一步來(lái)
7.1使用require.context將數(shù)據(jù)處理成扁平化
首先要處理成上圖那樣的結(jié)構(gòu),代碼都有注釋,難度也不高
//require.context() // 1. 一個(gè)要搜索的目錄, // 2. 一個(gè)標(biāo)記表示是否還要搜索其子目錄, // 3. 一個(gè)匹配文件的正則表達(dá)式。 let context = require.context("../pages", true, /.js$/); let paths = context.keys()//獲取了所有文件的路徑 let routes = paths.map(path => { //批量獲取引入的組件 let component = context(path).default; //組件擴(kuò)展屬性方便渲染菜單 let meta = component["meta"] || {} //console.log(path) //這個(gè)正則的目的 //因?yàn)榈刂肥?/Discover/Djradio/index.js這種類型的并不能直接使用,所以要進(jìn)行處理 //1.接去掉最前的"." 得到的結(jié)果是/Discover/Djradio/index.js //2.處理了還是不能直接用 因?yàn)槲覀兊念A(yù)期/Discover/Djradio,所以通過(guò)正則將index.js干掉了 //3.有可能后面的路徑不是文件夾 得到的結(jié)果是/Discover/abc.js,后綴名并不能用到路由配置的path屬性中,所以.js后綴名又用正則替換掉 path = path.substr(1).replace(/(/index.js|.js)$/, "") // console.log(path) return { path, component, meta } })
7.2 實(shí)現(xiàn)convertTree算法
上面處理好了數(shù)據(jù)后,我們封裝一個(gè)方法,專門用于處理扁平化數(shù)據(jù)變成樹(shù)形數(shù)據(jù),算法時(shí)間復(fù)雜度為O(n^2)
function convertTree(routes) { let treeArr = []; //1.處理數(shù)據(jù) 將每條數(shù)據(jù)的id和parent處理好 (俗稱 爸爸去哪兒了) routes.forEach(route => { let comparePaths = route.path.substr(1).split("/") // console.log(comparePaths) if (comparePaths.length === 1) { //說(shuō)明是根節(jié)點(diǎn),根節(jié)點(diǎn)不需要添加parent_id route.id = comparePaths.join("") } else { //說(shuō)明具有父節(jié)點(diǎn) //先處理自己的id route.id = comparePaths.join(""); //comparePaths除去最后一項(xiàng)就是parent_id comparePaths.pop() route.parent_id = comparePaths.join("") } }) //2.所有的數(shù)據(jù)都已經(jīng)找到了父節(jié)點(diǎn)的id,下面才是真正的找父節(jié)點(diǎn)了 routes.forEach(route => { //判斷當(dāng)前的route有沒(méi)有parent_id if (route.parent_id) { //有父節(jié)點(diǎn) //id===parent_id的那個(gè)route就是當(dāng)前route的父節(jié)點(diǎn) let target = routes.find(v => v.id === route.parent_id); //判斷父節(jié)點(diǎn)有沒(méi)有children這個(gè)屬性 if (!target.children) { target.children = [] } target.children.push(route) } else { treeArr.push(route) } }) return treeArr }
通過(guò)上述處理之后就可以得到樹(shù)形結(jié)構(gòu)啦
接下來(lái)我們只需要把數(shù)據(jù)導(dǎo)出去,在app上引入傳遞給CompileRouter組件就可以了
7.3 以后要注意的
以后只需要在pages中創(chuàng)建文件即可自動(dòng)實(shí)現(xiàn)路由的處理以及編譯了,不過(guò)對(duì)于嵌套級(jí)別的路由咱們別忘了要在路由組件加上CompileRouter組件,總結(jié)為亮點(diǎn)
- 創(chuàng)建路由頁(yè)面
- 嵌套路由的父級(jí)路由組件中加入
8.擴(kuò)展靜態(tài)屬性
我們當(dāng)前創(chuàng)建出來(lái)的效果是有了,但是如果我們用于渲染菜單的時(shí)候就會(huì)有問(wèn)題,沒(méi)有內(nèi)容可以用于渲染菜單,所以我們可以給組件上擴(kuò)展靜態(tài)屬性meta(也可以是別的),然后對(duì)我們的自動(dòng)化編譯代碼做一些小小的改動(dòng)就行了
組件
自動(dòng)化處理邏輯完整代碼
//require.context() // 1. 一個(gè)要搜索的目錄, // 2. 一個(gè)標(biāo)記表示是否還要搜索其子目錄, // 3. 一個(gè)匹配文件的正則表達(dá)式。 let context = require.context("../pages", true, /.js$/); let paths = context.keys()//獲取了所有文件的路徑 let routes = paths.map(path => { //批量獲取引入的組件 let component = context(path).default; //組件擴(kuò)展屬性方便渲染菜單 let meta = component["meta"] || {} //console.log(path) //這個(gè)正則的目的 //因?yàn)榈刂肥?/Discover/Djradio/index.js這種類型的并不能直接使用,所以要進(jìn)行處理 //1.接去掉最前的"." 得到的結(jié)果是/Discover/Djradio/index.js //2.處理了還是不能直接用 因?yàn)槲覀兊念A(yù)期/Discover/Djradio,所以通過(guò)正則將index.js干掉了 //3.有可能后面的路徑不是文件夾 得到的結(jié)果是/Discover/abc.js,后綴名并不能用到路由配置的path屬性中,所以.js后綴名又用正則替換掉 path = path.substr(1).replace(/(/index.js|.js)$/, "") // console.log(path) return { path, component, meta } }) //這種數(shù)據(jù)是扁平化的數(shù)據(jù),并不符合我們的路由規(guī)則 //需要做算法 盡可能將時(shí)間復(fù)雜度降低o(n)最好 //封裝一個(gè)convertTree算法 時(shí)間復(fù)雜度o(n^2) // console.log(routes) //id //parent_id function convertTree(routes) { let treeArr = []; //1.處理數(shù)據(jù) 將每條數(shù)據(jù)的id和parent處理好 (俗稱 爸爸去哪兒了) routes.forEach(route => { let comparePaths = route.path.substr(1).split("/") // console.log(comparePaths) if (comparePaths.length === 1) { //說(shuō)明是根節(jié)點(diǎn),根節(jié)點(diǎn)不需要添加parent_id route.id = comparePaths.join("") } else { //說(shuō)明具有父節(jié)點(diǎn) //先處理自己的id route.id = comparePaths.join(""); //comparePaths除去最后一項(xiàng)就是parent_id comparePaths.pop() route.parent_id = comparePaths.join("") } }) //2.所有的數(shù)據(jù)都已經(jīng)找到了父節(jié)點(diǎn)的id,下面才是真正的找父節(jié)點(diǎn)了 routes.forEach(route => { //判斷當(dāng)前的route有沒(méi)有parent_id if (route.parent_id) { //有父節(jié)點(diǎn) //id===parent_id的那個(gè)route就是當(dāng)前route的父節(jié)點(diǎn) let target = routes.find(v => v.id === route.parent_id); //判斷父節(jié)點(diǎn)有沒(méi)有children這個(gè)屬性 if (!target.children) { target.children = [] } target.children.push(route) } else { treeArr.push(route) } }) return treeArr } export default convertTree(routes) //獲取一個(gè)模塊 // console.log(p("./Discover/index.js").default) //目的是什么? //生成一個(gè)路由配置 // const routes = [ // { // path: "", // component, // children:[ // {path component} // ] // } // ]
寫在最后
其實(shí)上述的處理并不能作為應(yīng)用級(jí)別用于項(xiàng)目中,主要在于CompileRouter處理的不夠細(xì)致,下一期我將專門寫一篇如何處理CompileRouter用于鑒權(quán)等應(yīng)用在項(xiàng)目中
到此這篇關(guān)于react自動(dòng)化構(gòu)建路由的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)react自動(dòng)化構(gòu)建路由內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://juejin.cn/post/6953933167321415716