嵌套路由,从广义上来说,分为两种情况:一种是每个路由到的组件都有共有的内容,这时把共有的内容抽离成一个组件,变化的内容也是一个组件,两种组件组合嵌套,形成一个新的组件。另一种是子路由,路由到的组件内部还有路由。
对于共有的内容,典型的代表就是网页的侧边栏,假设侧边栏在左边,我们点击其中的按钮时,右侧的内容会变化,但不管右侧的内容怎么变化,左侧的侧边栏始终存在。这个侧边栏就是共有内容,如下图所示
这个共有内容要怎么处理? 首先想到的就是把这个功能提取出来,写成一个组件,然后再把这个组件依次应用到其它路由组件,如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; }