嵌套路由,從廣義上來說,分為兩種情況:一種是每個路由到的組件都有共有的內容,這時把共有的內容抽離成一個組件,變化的內容也是一個組件,兩種組件組合嵌套,形成一個新的組件。另一種是子路由,路由到的組件內部還有路由。
對於共有的內容,典型的代表就是網頁的側邊欄,假設側邊欄在左邊,我們點擊其中的按鈕時,右側的內容會變化,但不管右側的內容怎么變化,左側的側邊欄始終存在。這個側邊欄就是共有內容,如下圖所示

這個共有內容要怎么處理? 首先想到的就是把這個功能提取出來,寫成一個組件,然后再把這個組件依次應用到其它路由組件,如about, products 等。導航欄肯定是一個導航,不過這里我們要使用navLink 組件,因為從圖片中可以看到它有高亮顯示,我們要設計一個高亮顯示的樣式。新建一個menus.js 文件,來寫這個導航組件
import React from 'react' // 引入NavLink 組件 import { NavLink } from "react-router-dom"; import './menus.css' // 高亮的樣式,表示我們在哪個導航下 const selectedStyle = { backgroundColor: 'white', color: 'slategray' } // navLink, activeStyle 點擊高亮顯示當前標簽。 export const MainMenu = () => ( <nav className='main-menu'> <NavLink to='/'>首頁</NavLink> <NavLink to='/about' activeStyle = {selectedStyle}>關於我們</NavLink> <NavLink to='/events' activeStyle = {selectedStyle}>企業事件</NavLink> <NavLink to='/products' activeStyle = {selectedStyle}>公司產品</NavLink> <NavLink to='/contact' activeStyle = {selectedStyle}>聯系我們</NavLink> </nav> )
可以看到導航的高亮樣式可以在NavLink 組件中直接設置,這也是Link 和NavLink的區別。這里還寫了一點樣式,在menus.css 文件中。
/* 頁面左側主要導航 */ .main-menu { display: flex; flex-direction: column; height: 100%; width: 20%; min-width: 20%; background-color: slategray; color: ghostwhite; } .main-menu a { color: ghostwhite; text-align: center; padding: 1em; font-size: 1.5em; }
按照上面的說法,我們把這個組件應用到其它組件中,在pages.js中引入該組件,並修改Events,Products,
// 企業事件內容 export const Events = () => ( <div> <MainMenu></MainMenu> <section className="events"> <h1>企業大事件</h1> </section> </div> ) // 公司產品 export const Products = () => ( <div> <MainMenu></MainMenu> <section className="products"> <h1>公司產品:主要經營 手機、電腦</h1> </section> </div> ) // 聯系我們 export const Contact = () => ( <div> <MainMenu></MainMenu> <section className="contact"> <h1>聯系我們</h1> <p>公司電話:0755 - 12345678</p> </section> </div> )
這時你會發現,相同的代碼復制了4遍,還可以接受,畢竟只有一個共有組件。但如果about, home 這些組件有好多共有的部分,我們這樣一遍一遍的復制就有點麻煩了。所以還要對page.js文件這些組件相同的內容進行進一步的抽取。抽取的形式應該是
<div>
<MainMenu></MainMenu>
// 變化的部分
</div>
現在最主要的部分就是這些變化的部分要怎么處理,想到了其實也很簡單,因為React 有一個children 屬性, 直接把這些變化的部分寫成props.childern 就可以了。這時你會發現,這個抽取的組件像一個布局模板, 比如單頁面應用時的頁眉和頁腳這些共有的部分,也可以放到這個模版中。新建一個template.js 文件
import React from 'react' import { MainMenu } from "./menus"; export const Template = (props) => ( <div className = 'page'> <MainMenu></MainMenu> {props.children} </div> )
上面的代碼中加了一個樣式類page, 在pages.css 中添加一個 page樣式,
.page { display: flex; justify-content: space-between; height: 100%; margin-top: 20px; }
現在再在pages.js中的相應組件中應用Template組件
import { Template } from "./template";// 企業事件內容
export const Events = () => (
<Template>
<section className="events">
<h1>企業大事件</h1>
</section>
</Template>
)
// 公司產品
export const Products = () => (
<Template>
<section className="products">
<h1>公司產品:主要經營 手機、電腦</h1>
</section>
</Template>
)
// 聯系我們
export const Contact = () => (
<Template>
<section className="contact">
<h1>聯系我們</h1>
<p>公司電話:0755 - 12345678</p>
</section>
</Template>
)
這時效果就達到了,左側的側邊欄始終存在。現在 我們再來實現一下子路由。
子路由: 就是當路由匹配成功后,它會渲染出一個組件。這個組件中還有路由,這個路由就是子路由。比如: 我進入到about頁面,about 組件渲染出來,而about 組件中,還有history, service 要展示,它還需要路由,about 組件中的路由就是子路由,如下圖所示

about組件上面的導航條,我們還是使用NavLink實現,導航條下面的內容肯定是路由系統。對於這里的路由來說,我們要注意的就是它的path屬性,因為它是子路由,我們首先要進入到父組件about下面,才能顯示這個路由系統,所以它前面的路徑都要加about, 匹配公司服務, 它的path 肯定是’/about/service’; 其它三個對應的則是 about/company, about/location, about/history, 但如果四個路由都這么匹配,就會出現一個問題,首次進入到about 組件時,它什么都不會顯示。就是說,我們點擊左側的側邊欄中的 ‘關於我們’,它不會顯示任何內容。這里可以這么處理,點擊 ‘關於我們’,它肯定會顯示四個子路由中的 一個,比如顯示公司簡介, 我們就可以把公司簡介的路由直接設置成about, 對於子路由來說,父路由進來顯示的默認頁面,就可以設置和父路由一樣。
導航條,我們寫到menus.js 文件里面,命名為AboutMenu
export const AboutMenu = () => (
<ul className="about-menu">
<li>
<NavLink to='/about' exact activeStyle ={selectedStyle}>公司簡介</NavLink>
</li>
<li>
<NavLink to='/about/history' activeStyle ={selectedStyle}>公司歷史</NavLink>
</li>
<li>
<NavLink to='/about/services' activeStyle ={selectedStyle}>公司服務</NavLink>
</li>
<li>
<NavLink to='/about/location' activeStyle ={selectedStyle}>企業位置</NavLink>
</li>
</ul>
)
我們再在pages.js中修改about組件的內容,使其包含路由組件
export const About = () => (
<Template>
<section className="about">
<AboutMenu></AboutMenu>
<Route path='/about' exact component={Company}/>
<Route path='/about/history' component={History}/>
<Route path='/about/services' component={Services}/>
<Route path='/about/location' component={Location}/>
</section>
</Template>
)
同時再簡單地定義Company, History 等4個顯示組件。
export const Services = () => ( <section> <p>公司服務</p> </section> ) export const Location = () => ( <section> <p>公司位置</p> </section> ) export const Company = () => ( <section> <p>公司簡介</p> </section> ) export const History = () => ( <section> <p>公司歷史</p> </section> )
這時就實現了子路由的功能。
動態路由:就是在匹配路徑path 的后面加上冒號 + 參數, 如path ="products/:id". 比如我們進入產品頁面,它有手機,電腦等產品,我們點擊單個產品,肯定進入到產品詳情頁面,我們為每一個產品都建一個產品詳情組件,有點不太現實,因為產品太多,寫的大累,再說,產品種類是動態增加的,增加一個產品,就增加一個組件,維護起來也太累。最好的辦法就是,它們都跳轉到一個詳情組件,根據不同的產品渲染動態渲染不同的內容, 那么產品詳情組件就要接受一個參數表示產品類型,這個類型是動態的,所以要用一個變量,路由的格式就是details/:type, 那組件內部怎么得到這個參數呢,我們之前說過,當渲染組件時,路由會給我們組件注入3個參數,這里使用match 就可以了,它有一個params屬性,就是專門獲取動態路由參數的。
我們在products組件內部,寫兩個link, 用於導航到產品詳情,同時寫一個產品詳情頁組件。
// 公司產品 export const Products = () => ( <Template> <section className="products"> <Link to='/details/telphone'>手機</Link> <Link to='/details/computer'>電腦</Link> </section> </Template> ) // 產品詳情組件 export const Details = (props) => { console.log(props.match.params); return <p>這是 {props.match.params.type}詳情內容</p> }
但產品詳情的路由的定義要放到什么地方呢?由於產品詳情要占據整個頁面,它和 Events, Products 組件是一個級別的,所以要放到App組件 中, 在app.js文件中路由下面增加一條路由,<Route path='/details/:type' component={Details}></Route>
<Switch>
<Route path='/' exact component={Home}/>
<Route path='/about' component={About}/>
<Route path='/contact' component={Contact}/>
<Route path='/products' component={Products}/>
<Route path='/events' component={Events}/>
<Route path='/details/:type' component={Details}></Route>
<Route component={NotFound404}/>
</Switch>
現在可以實現動態路由了。
最后一點是重定向路由組件<Redirect>, 它最基本使用就是一個to 屬性,和link的to 屬性一個意思,到什么地方去。<Redirect to=’/about’>, 就是重定向到about組件。當在Switch 組件下面,它還有一個from屬性,它的接受的參數和to 一樣,是一個路徑,表示從哪里來。在這里,假設用戶在瀏覽器地址欄中輸入history, 我們把它重定向about/history, 就可以這么寫
<Redirect from='/history' to='about/history'></Redirect>
它還有一種使用場景就是,已有路徑的重定向,比如,當用戶訪問首頁的時候,把它重定向到產品頁面。這里要知道的是,Route 組件的除了接受component,還可以接受一個render 函數,render函數渲染出一個組件。
<Route path='/' exact render={() => <Redirect to='/products' />} />
整個項目內容如下,就是在src目錄下新建了 menus.js, pages.js, template.js,pages.css, menus.css 文件,修改了App.js 文件
App.js
import React from 'react' import { HashRouter, Route, Switch, Redirect } from 'react-router-dom' // 引入展示組件 import { About, Contact, Home, Products, Events, NotFound404, Details } from './pages'; function App() { return ( <HashRouter> <div> <Switch> <Route path='/' exact component={Home}/> <Route path='/about' component={About}/> <Route path='/contact' component={Contact}/> <Route path='/products' component={Products}/> <Route path='/events' component={Events}/> <Redirect from='/history' to='about/history'></Redirect> <Route path='/details/:type' component={Details}></Route> <Route component={NotFound404}/> </Switch> </div> </HashRouter> ) } export default App
menus.js
import React from 'react' // 引入NavLink 組件 import { NavLink } from "react-router-dom"; import './menus.css' // 高亮的樣式,表示我們在哪個導航下 const selectedStyle = { backgroundColor: 'white', color: 'slategray' } // navLink, activeStyle 點擊高亮顯示當前標簽。 export const MainMenu = () => ( <nav className='main-menu'> <NavLink to='/'>首頁</NavLink> <NavLink to='/about' activeStyle = {selectedStyle}>關於我們</NavLink> <NavLink to='/events' activeStyle = {selectedStyle}>企業事件</NavLink> <NavLink to='/products' activeStyle = {selectedStyle}>公司產品</NavLink> <NavLink to='/contact' activeStyle = {selectedStyle}>聯系我們</NavLink> </nav> ) export const AboutMenu = () => ( <ul className="about-menu"> <li> <NavLink to='/about' exact activeStyle ={selectedStyle}>公司簡介</NavLink> </li> <li> <NavLink to='/about/history' activeStyle ={selectedStyle}>公司歷史</NavLink> </li> <li> <NavLink to='/about/services' activeStyle ={selectedStyle}>公司服務</NavLink> </li> <li> <NavLink to='/about/location' activeStyle ={selectedStyle}>企業位置</NavLink> </li> </ul> )
pages.js
import React from 'react' import { Link, Route } from "react-router-dom"; import './pages.css'; import { Template } from "./template"; import { AboutMenu } from "./menus"; // 首頁內容 export const Home = () => ( <section className="home"> <h1>企業網站</h1> <nav> {/* 添加了四個導航組件Link */} <Link to='/about'>關於我們</Link> <Link to='/events'>企業事件</Link> <Link to='/products'>公司產品</Link> <Link to='/contact'>聯系我們</Link> </nav> </section> ) // 企業事件內容 export const Events = () => ( <Template> <section className="events"> <h1>企業大事件</h1> </section> </Template> ) // 公司產品 export const Products = () => ( <Template> <section className="products"> <Link to='/details/telphone'>手機</Link> <Link to='/details/computer'>電腦</Link> </section> </Template> ) // 產品詳情組件 export const Details = (props) => { console.log(props.match.params); return <p>這是 {props.match.params.type}詳情內容</p> } // 聯系我們 export const Contact = () => ( <Template> <section className="contact"> <h1>聯系我們</h1> <p>公司電話:0755 - 12345678</p> </section> </Template> ) // 關於我們 export const About = () => ( <Template> <section className="about"> <AboutMenu></AboutMenu> <Route path='/about' exact component={Company}/> <Route path='/about/history' component={History}/> <Route path='/about/services' component={Services}/> <Route path='/about/location' component={Location}/> </section> </Template> ) // 沒有匹配成功的404組件 export const NotFound404 = (props) => ( <div className="whoops-404"> <h1>沒有頁面可以匹配</h1> </div> ) // 4個子路由對應的顯示組件 const Services = () => ( <section> <p>公司服務</p> </section> ) const Location = () => ( <section> <p>公司位置</p> </section> ) const Company = () => ( <section> <p>公司簡介</p> </section> ) const History = () => ( <section> <p>公司歷史</p> </section> )
template.js
import React from 'react' import { MainMenu } from "./menus"; export const Template = (props) => ( <div className = 'page'> <MainMenu></MainMenu> {props.children} </div> )
menus.css
/* 頁面左側主要導航 */ .main-menu { display: flex; flex-direction: column; height: 100%; width: 20%; min-width: 20%; background-color: slategray; color: ghostwhite; } .main-menu a { color: ghostwhite; text-align: center; padding: 1em; font-size: 1.5em; } .about-menu { width: 100%; margin: 0; padding: 0; list-style-type: none; display: flex; background-color: slategray; } .about-menu > li { flex-grow: 1; } .about-menu > li > a { display: block; width: calc(100% - 1em); text-align: center; text-decoration: none; color: ghostwhite; padding: 0.5em; }
pages.css
html, body, #root { height: 100%; } h1 { font-size: 3em; color: slategray; } /* home 組件 */ .home { height: 100%; display: flex; flex-direction: column; align-items: center; } .home > nav { display: flex; justify-content: space-around; padding: 1em; width: calc(100% - 2em); border-top: dashed 0.5em ghostwhite; border-bottom: dashed 0.5em ghostwhite; background-color: slategray; } .home > nav a { font-size: 2em; color: ghostwhite; flex-basis: 200px; } /* 其它組件 */ section.events, section.products, section.contact { flex-grow: 1; margin: 1em; display: flex; justify-content: center; align-items: center; } /* 404頁面 */ .whoops-404 { position: fixed; top: 0; left: 0; z-index: 99; display: flex; width: 100%; height: 100%; margin: 0; justify-content: center; align-items: center; background-color: darkred; color: ghostwhite; font-size: 1.5em; } .page { display: flex; justify-content: space-between; height: 100%; padding-top: 20px; background: #f3f7ff; } /* about 組件 */ .about { flex-grow: 1; margin: 1em; display: flex; flex-direction: column; } .about > section { text-align: center; }
