相关理解
SPA
- 单页Web应用(single page web application,SPA)。
- 整个应用只有一个完整的页面。
- 点击页面中的链接不会刷新页面,只会做页面的局部更新。
- 数据都需要通过ajax请求获取, 并在前端异步展现。
路由
-
什么是路由?
- 一个路由就是一个映射关系
(key:value)
key
为路径, value
可能是function
或component
-
路由分类
- 后端路由:
- 理解:
value
是function
, 用来处理客户端提交的请求
- 注册路由:
router.get(path, function(req, res))
- 工作过程:当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
- 前端路由:
- 浏览器端路由,
value
是component
,用于展示页面内容
- 注册路由:
<Route path="/test" component={Test}>
- 工作过程:当浏览器的
path
变为/test
时, 当前路由组件就会变为Test
组件
-
前端路由的基石:history
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>前端路由的基石_history</title> </head> <body> <a href="http://www.atguigu.com" onclick="return push('/test1') ">push test1</a><br><br> <button onClick="push('/test2')">push test2</button><br><br> <button onClick="replace('/test3')">replace test3</button><br><br> <button onClick="back()"><= 回退</button> <button onClick="forword()">前进 =></button>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script> <script type="text/javascript"> let history = History.createHashHistory()
function push (path) { history.push(path) return false }
function replace (path) { history.replace(path) }
function back() { history.goBack() }
function forword() { history.goForward() }
history.listen((location) => { console.log('请求路由路径变化了', location) }) </script> </body> </html>
|
react-router-dom
- react的一个插件库;
- 专门用来实现一个SPA应用;
- 基于react的项目基本都会用到此库。
路由的基本使用
原理:点击导航链接引起路径变化,路径变化被router监测到,进行匹配组件;单页面,多组件。
安装React Router 5
:
1
| npm i react-router-dom@5
|
简单案例
组件:
About组件(Home组件同理)
1 2 3 4 5 6 7 8 9 10
| import React, { Component } from 'react'
export default class About extends Component { render() { return ( <h3>我是About的内容</h3> ) } }
|
App组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import React, { Component } from 'react' import {Link, Route} from 'react-router-dom' import About from './components/About' import Home from './components/Home'
export default class App extends Component {
render() { return ( <div> <div className="row"> <div className="col-xs-offset-2 col-xs-8"> <div className="page-header"><h2>React Router Demo</h2></div> </div> </div> <div className="row"> <div className="col-xs-2 col-xs-offset-2"> <div className="list-group"> {/* 原生html中,靠a标签跳转不同的页面 */} {/* <a className="list-group-item" href="./about.html">About</a> <a className="list-group-item active" href="./home.html">Home</a> */}
{/* 在React中靠路由链接实现 --- 编写路由链接 */} <Link className="list-group-item" to="/about">About</Link> <Link className="list-group-item" to="/home">Home</Link> </div> </div> <div className="col-xs-6"> <div className="panel"> <div className="panel-body"> {/* 注册路由 */} <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </div> </div> </div> </div> </div> ) } }
|
index.js
1 2 3 4 5 6 7 8 9
| import React from 'react' import ReactDOM from 'react-dom' import App from './App' import {BrowserRouter} from 'react-router-dom'
ReactDOM.render( <BrowserRouter><App /></BrowserRouter>, document.getElementById('root') )
|
注意:
- 整个网页由一个
router
管理,即Link
标签与Route
标签要由同一个BrowserRouter
标签包裹,为了方便,可以直接将App
组件包裹
HashRouter
使用#
接路由路径,#
后面的内容不会传给服务器
效果:
路由组件和一般组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import React, { Component } from 'react' import {Link, Route} from 'react-router-dom' import About from './pages/About' import Home from './pages/Home' import Header from './components/Header'
export default class App extends Component {
render() { return ( <div> <div className="row"> <div className="col-xs-offset-2 col-xs-8"> <Header/> </div> </div> <div className="row"> <div className="col-xs-2 col-xs-offset-2"> <div className="list-group"> {/* 原生html中,靠a标签跳转不同的页面 */} {/* <a className="list-group-item" href="./about.html">About</a> <a className="list-group-item active" href="./home.html">Home</a> */}
{/* 在React中靠路由链接实现 --- 编写路由链接 */} <Link className="list-group-item" to="/about">About</Link> <Link className="list-group-item" to="/home">Home</Link> </div> </div> <div className="col-xs-6"> <div className="panel"> <div className="panel-body"> {/* 注册路由 */} <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </div> </div> </div> </div> </div> ) } }
|
-
写法不同
- 一般组件:
<Demo/>
- 路由组件:
<Route path="/demo" component={Demo}/>
- 值得注意的是,
React Router 6
中component
属性已替换为element
,参数中可以写为一般组件的形式
-
存放位置不同
- 一般组件:
components
- 路由组件:
pages
-
接收到的props
不同
-
一般组件:写组件标签时传递的内容
-
路由组件:接收到固定的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| history: go: ƒ go(n) goBack: ƒ goBack() goForward: ƒ goForward() push: ƒ push(path, state) replace: ƒ replace(path, state)
location: pathname: "/demo" search: "" state: undefined
match: params: {} path: "/demo" url: "/demo"
|
-
NavLink
NavLink
被点击后,会为当前样式追加一个类,默认类名为active
,可通过activeClassName
修改
例如,在App.css中编写样式(此处使用!important
是因为bootstrap的优先级过高:
1 2 3 4
| .my-active { background-color: olivedrab !important; color: white !important; }
|
NavLink如下所示:
1 2
| <NavLink activeClassName='my-active' className="list-group-item" to="/about">About</NavLink> <NavLink activeClassName='my-active' className="list-group-item" to="/home">Home</NavLink>
|
效果:
二次封装NavLink
封装一个MyNavLink
,用于指定固定的className
与activeClassName
,属于一般组件
1 2 3 4 5 6 7 8 9 10
| import React, { Component } from 'react' import {NavLink} from 'react-router-dom'
export default class MyNavLink extends Component { render() { return ( <NavLink activeClassName='my-active' className="list-group-item" {...this.props} /> ) } }
|
使用时直接写MyNavLink
标签即可:
1 2
| <MyNavLink to="/about">About</MyNavLink> {/* 标签体内容是特殊的标签属性: children */} <MyNavLink to="/home">Home</MyNavLink>
|
Switch
注意:在React Router 6
中,Switch
已被Routes
替代
使用<Switch></Switch>
将注册的路由包裹起来,匹配路径时只匹配最先出现的组件
1 2 3 4
| <Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </Switch>
|
- 通常情况下,
path
和component
是一一对应的关系
Switch
可以提高路由匹配效率(单一匹配)
Redirect
当所有路由都无法匹配时,跳转到Redirect指定的路由,一般写在所有路由注册的最下方
1 2 3 4 5 6
| {} <Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Redirect to="/about" /> </Switch>
|
多级路径刷新页面样式丢失
原因:href="./css/bootstrap.css"
是从当前路径开始寻找,当多级路径写为如<MyNavLink to="/abc/home">Home</MyNavLink>
时,将在/abc
下寻找css文件,故刷新后样式会丢失
解决方法:
public/index.html
中,css的引入使用<link rel="stylesheet" href="/css/bootstrap.css">
- 常用
public/index.html
中,css的引入使用<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
- 常用
- 使用
HashRouter
路由的模糊匹配与严格匹配
-
React Router 5
默认使用的是模糊匹配,即:输入的路径
必须要包含匹配的路径
,且顺序要一致
-
开启严格匹配:
1 2
| <Route exact={true} path="/about" component={About}/> <Route exact path="/home" component={Home}/>
|
-
不要随意开启严格匹配,有需要的时候再使用,开启会导致无法匹配二级路由
嵌套路由
- 注册子路由时要写上父路由的
path
值,否则无法匹配到父级路由
- 路由的匹配是按照注册路由的顺序进行的
Home组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import React, { Component } from 'react' import { Switch, Route, Redirect } from 'react-router-dom' import MyNavLink from '../../components/MyNavLink' import News from './News' import Message from './Message'
export default class Home extends Component { render() { return ( <div> <h3>我是Home的内容</h3> <div> <ul className="nav nav-tabs"> <li> <MyNavLink to="/home/news">News</MyNavLink> </li> <li> <MyNavLink to="/home/message">Message</MyNavLink> </li> </ul> {/* 注册路由 */} <Switch> <Route path="/home/news" component={News} /> <Route path="/home/message" component={Message} /> <Redirect to="/home/news" /> </Switch> </div> </div> ) } }
|
向路由组件传参
准备工作:声明Detail组件,用来展示message的详情,像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React, { Component } from 'react'
const detailData = [ {id: '01', content: '小爱同学'}, {id: '02', content: '嘿Siri'}, {id: '03', content: '你好小娜'} ]
export default class Detail extends Component { render() { return ( <ul> <li>id: </li> <li>title: </li> <li>content: </li> </ul> ) } }
|
传递params参数
- 路由链接(携带参数):
<Link to='/demo/test/tom/18'}>详情</Link>
- 注册路由(声明接收):
<Route path="/demo/test/:name/:age" component={Test}/>
- 接收参数:
this.props.match.params
在Detail组件中console.log(this.props)
可以看到如图所示的属性:
Message组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import React, { Component } from 'react' import { Link, Route } from 'react-router-dom' import Detail from './Detail'
export default class Message extends Component {
state = { messageArr: [ {id: '01', title: '消息1'}, {id: '02', title: '消息2'}, {id: '03', title: '消息3'} ] }
render() { const {messageArr} = this.state return ( <div> <ul> {messageArr.map((msgObj) => { return ( <li key={msgObj.id}> {/* 向路由组件传递params参数 */} <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> </li> ) })} </ul> <hr /> {/* 声明接收params参数 */} <Route path="/home/message/detail/:id/:title" component={Detail} /> </div> ) } }
|
Detail组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React, { Component } from 'react'
const detailData = [ {id: '01', content: '小爱同学'}, {id: '02', content: '嘿Siri'}, {id: '03', content: '你好小娜'} ]
export default class Detail extends Component { render() { const {id, title} = this.props.match.params const findContent = detailData.find((detailObj) => { return detailObj.id === id }) return ( <ul> <li>id: {id}</li> <li>title: {title}</li> <li>content: {findContent.content}</li> </ul> ) } }
|
传递search参数
(传递的时候省心一点,接收的时候麻烦一点)
- 路由链接(携带参数):
<Link to='/demo/test?name=tom&age=18'}>详情</Link>
- 注册路由(无需声明,正常注册即可):
<Route path="/demo/test" component={Test}/>
- 接收参数:
this.props.location.search
- 备注:获取到的
search
是urlencoded
编码字符串,需要借助querystring解析
在Detail组件中console.log(this.props)
可以看到如图所示的属性:
Message组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| render() { const {messageArr} = this.state return ( <div> <ul> {messageArr.map((msgObj) => { return ( <li key={msgObj.id}> {/* 向路由组件传递search参数 */} <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> </li> ) })} </ul> <hr /> {/* search参数无需声明接收,正常注册路由即可 */} <Route path="/home/message/detail" component={Detail} /> </div> ) }
|
Detail组件
1 2 3
| const {search} = this.props.location const {id, title} = qs.parse(search.slice(1))
|
不过qs好像要过时了:
传递state参数
传递的内容不会在地址栏展示,与组件的状态(state)不同
- 路由链接(携带参数):
<Link to={{pathname: '/demo/test', state: {name: 'tom', age: 18}}}>详情</Link>
- 注册路由(无需声明,正常注册即可):
<Route path="/demo/test" component={Test}/>
- 接收参数:
this.props.location.state
- 备注:刷新也可以保留住参数,但清除浏览器数据后不可
- state参数默认
replace=true
(猜想)
在Detail组件中console.log(this.props)
可以看到如图所示的属性:
Message组件
1 2 3 4 5
| {} <Link to={{pathname: '/home/message/detail', state: {id: msgObj.id, title: msgObj.title}}}>{msgObj.title}</Link>
{} <Route path="/home/message/detail" component={Detail} />
|
Detail组件
1 2
| const {id, title} = this.props.location.state || {}
|
编程式路由导航
借助this.props.history
的API实现跳转、前进、后退:
1 2 3 4 5
| -this.prosp.history.push() -this.prosp.history.replace() -this.prosp.history.goBack() -this.prosp.history.goForward() -this.prosp.history.go()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| import React, { Component } from 'react' import { Link, Route } from 'react-router-dom' import Detail from './Detail'
export default class Message extends Component {
state = { messageArr: [ {id: '01', title: '消息1'}, {id: '02', title: '消息2'}, {id: '03', title: '消息3'} ] }
pushShow = (id, title) => {
this.props.history.push('/home/message/detail', {id, title}) }
replaceShow = (id, title) => {
this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)
this.props.history.replace('/home/message/detail', {id, title}) }
render() { const {messageArr} = this.state return ( <div> <ul> {messageArr.map((msgObj) => { return ( <li key={msgObj.id}> {/* 向路由组件传递params参数 */} {/* <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> */} <button onClick={() => this.pushShow(msgObj.id, msgObj.title)}>push查看</button> <button onClick={() => this.replaceShow(msgObj.id, msgObj.title)}>replace查看</button>
{/* 向路由组件传递search参数 */} {/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */} {/* 向路由组件传递state参数 */} <Link to={{pathname: '/home/message/detail', state: {id: msgObj.id, title: msgObj.title}}}>{msgObj.title}</Link> </li> ) })} </ul> <hr /> {/* 声明接收params参数 */} {/* <Route path="/home/message/detail/:id/:title" component={Detail} /> */}
{/* search参数无需声明接收,正常注册路由即可 */} {/* <Route path="/home/message/detail" component={Detail} /> */}
{/* state参数无需声明接收,正常注册路由即可 */} <Route path="/home/message/detail" component={Detail} /> </div> ) } }
|
路由的跳转更加灵活:
需求:切换到News路由后,等待3秒,自动跳转到Message
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React, { Component } from 'react'
export default class News extends Component {
componentDidMount() { setTimeout(() => { this.props.history.push('/home/message') }, 3000) }
render() { return ( <ul> <li>news001</li> <li>news002</li> <li>news003</li> </ul> ) } }
|
withRouter
withRouter
可以加工一般组件,让一般组件具备路由组件所特有的API,其返回值是一个新组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React, { Component } from 'react' import { withRouter } from 'react-router-dom'
class Header extends Component {
back = () => { this.props.history.goBack() }
forward = () => { this.props.history.goForward() }
render() { return ( <div className="page-header"> <h2>React Router Demo</h2> <button onClick={this.back}>后退</button> <button onClick={this.forward}>前进</button> </div> ) } }
export default withRouter(Header)
|
Browser Router
与Hash Router
- 底层原理不同:
- BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
- HashRouter使用的是URL的哈希值。
path
表现形式不同:
BrowserRouter
的路径中没有#
,例如:localhost:3000/demo/test
HashRouter
的路径包含#
,例如:localhost:3000/#/demo/test
- 刷新后对路由
state
参数的影响:
BrowserRouter
没有任何影响,因为state
保存在history
对象中。
HashRouter
刷新后会导致路由state
参数的丢失!!!
- 备注:
HashRouter
可以用于解决一些路径错误相关的问题。