在使用`react-router-dom`在編寫項目的時候有種感覺就是,使用起來非常的方便,但是若是維護起來,那便是比較麻煩了,因為各大路由分散在各個組件中. 所以我們就會想到,使用`react-router-dom`中提供的`config`模式來編寫我們的路由,這樣寫的好處就是我們可以將邏輯集中在一處,配置路由比較方便
## **項目地址**
[https://gitee.com/d718781500/autoRouter](https://link.zhihu.com/?target=https%3A//gitee.com/d718781500/autoRouter)
## **1.路由集中式**
我們先將下列數據定義在`/src/router/index.js`中
在react的路由官方文檔中就提供了配置集中式路由的案例,大致是這樣的仿照`vue`的路由,生成一個配置文件,預期是這樣的
```js
//需要一個路由的配置,它是一個數組
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目錄結構**
```js
├─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`
我們來寫段偽代碼來說明一下
```js
//引入路由基本組件(要在項目中安裝 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`
```js
//這個文件通過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`
```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`路由為例子,首先我們看下結構圖
![img](https://pic4.zhimg.com/80/v2-99aebf28aca62fa66c48587f6897068b_720w.jpg)
圖上的`index.js`就是`Discover`這個視圖組件了,也是嵌套路由的`父級路由`,所以我們只需要在這個`index.js`中繼續使用`CompileRouter`就可以了
```js
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(routes)
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
```
所以我們以后記住,只要是有嵌套路由我們要做兩件事
1. 配置routes
2. 在嵌套路由的父級路由中再次使用`CompileRouter`,并且傳入`routes`即可
## **6.** **require.context**
上面我們實現了一個路由集中式的配置,但是我們會發現一個問題
![img](https://pic3.zhimg.com/80/v2-93403806cb935bd4dbeb50a6e770f66e_720w.jpg)
引入了很多的組件,實際上,在項目中引入的更多,如果一個一個引入,對我們來說是災難性的,所以我們可以使用`webpack`提供的一個很好用的api,`require.context`我們先說說它是怎么使用的吧
自動化導入`require.context`方法,使用這個方法可以減少繁瑣的組件引入,而且可以深度的遞歸目錄,做到import做不到的事情 下面我們來看一下這個方法是如何使用的
### **使用**
你可以通過 `require.context()` 函數來創建自己的 context。
可以給這個函數傳入4個參數:
1. 一個要搜索的目錄,
2. 一個標記表示是否還要搜索其子目錄,
3. 一個匹配文件的正則表達式。
4. mode 模塊加載模式,常用值為 sync、lazy、lazy-once、eager
- `sync` 直接打包到當前文件,同步加載并執行
`lazy` 延遲加載會分離出單獨的 chunk 文件
`lazy-once` 延遲加載會分離出單獨的 chunk 文件,加載過下次再加載直接讀取內存里的代碼。
`eager` 不會分離出單獨的 chunk 文件,但是會返回 promise,只有調用了 promise 才會執行代碼,可以理解為先加載了代碼,但是我們可以控制延遲執行這部分代碼。
webpack 會在構建中解析代碼中的 `require.context()` 。
語法如下:
```js
require.context(
directory,
(useSubdirectories = true),
(regExp = /^\.\/.*$/),
(mode = 'sync')
);
```
示例:
```js
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("...",true,"xxx")
p.resolve("一個路徑")
- `keys` 也是一個函數,它返回一個數組,由所有可能被此 context module 處理的請求(譯者注:參考下面第二段代碼中的 key)組成。
`require.context`的返回值是一個函數,我們可以在函數中傳入文件的路徑,就可以得到模塊化的組件了
```js
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)
```
### **總結**
雖然上面有很多api和返回的值,我們只拿兩個來做說明
1. keys方法,這個可以獲取所有模塊的路徑,返回的是一個數組
let context = require.context("../pages", true, /\.js$/);
let paths = context.keys()*//獲取了所有文件的路徑*
2. 獲取路徑下所有的模塊
let context = require.context("../pages", true, /\.js$/);
let paths = context.keys()*//獲取了所有文件的路徑*
let routes = paths.map(path => {
*//批量獲取引入的組件*
let component = context(path).default;
console.log(component)
})
掌握這兩個就可以了,下面我們來繼續處理
## **7.扁平數據轉換為樹形結構的(convertTree算法)**
這個算法的名字是我自己起的,首先我們要明白為甚么需要將數據轉換成tree
我們的預期的`routes`數據應該是下面這樣的
```js
//目的是什么?
//生成一個路由配置
const routes = [
{
path: "",
component:xxx
children:[
{
path:"xxx"
component:xxx
}
]
}
]
```
但其實我們使用`require.context`處理之后的數據是這樣的
![img](https://pic1.zhimg.com/80/v2-7562f0762bc6f1e97c9c7b7e128ef618_720w.jpg)
可以看到這個數據是完全`扁平化`的,沒有任何的嵌套,所以我們第一步就是要實現將這種扁平化的數據轉換為符合我們預期的`樹形`結構,下面我們一步一步來
### **7.1使用require.context將數據處理成扁平化**
首先要處理成上圖那樣的結構,代碼都有注釋,難度也不高
```js
//require.context()
// 1. 一個要搜索的目錄,
// 2. 一個標記表示是否還要搜索其子目錄,
// 3. 一個匹配文件的正則表達式。
let context = require.context("../pages", true, /\.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)`
```js
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.parent_id);
//判斷父節點有沒有children這個屬性
if (!target.children) {
target.children = []
}
target.children.push(route)
} else {
treeArr.push(route)
}
})
return treeArr
}
```
通過上述處理之后就可以得到樹形結構啦
![img](https://pic3.zhimg.com/80/v2-74337231c1b570e8532025dd75a56aaa_720w.jpg)
接下來我們只需要把數據導出去,在app上引入傳遞給`CompileRouter`組件就可以了
### **7.3** **以后要注意的**
以后只需要在pages中創建文件即可自動實現路由的處理以及編譯了,不過對于嵌套級別的路由咱們別忘了要在路由組件加上CompileRouter組件,總結為亮點
1. 創建路由頁面
2. 嵌套路由的父級路由組件中加入
## **8.擴展靜態屬性**
我們當前創建出來的效果是有了,但是如果我們用于渲染`菜單`的時候就會有問題,沒有內容可以用于渲染菜單,所以我們可以給組件上擴展`靜態屬性meta(也可以是別的)`,然后對我們的自動化編譯代碼做一些小小的改動就行了
### **組件**
### **自動化處理邏輯完整代碼**
```js
//require.context()
// 1. 一個要搜索的目錄,
// 2. 一個標記表示是否還要搜索其子目錄,
// 3. 一個匹配文件的正則表達式。
let context = require.context("../pages", true, /\.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
}
})
//這種數據是扁平化的數據,并不符合我們的路由規則
//需要做算法 盡可能將時間復雜度降低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) {
//說明是根節點,根節點不需要添加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.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`用于`鑒權`等應用在項目中
**-** **End** **-**
更多關于“html5培訓”的問題,歡迎咨詢千鋒教育在線名師。千鋒已有十余年的培訓經驗,課程大綱更科學更專業,有針對零基礎的就業班,有針對想提升技術的提升班,高品質課程助理你實現夢想。