本文参考教程:[尚硅谷2021版React技术全家桶]
本文上接:
[React学习笔记1(React概念)]
[React学习笔记2(React脚手架、组件通信与网络请求库)]
本文顺序与视频不同,更适合学习过的复习,初学还是建议看视频
react内置路由器库为React-Router库,其又细分为如下
- react-router:核心组件
- react-router-dom:应用于浏览器端开发的路由库(单独使用包含了react-router的核心部分)
- react-router-native:应用于native端开发的路由
本文只涉及react-router-dom的使用
安装库(命令行下,脚手架默认没有安装):npm install react-router-dom --save
(1)整个应用只有一个完整的页面。
(2)点击页面中的链接不会刷新页面,只会做页面的局部更新。
(3)数据都需要通过ajax请求获取, 并在前端异步展现。
path路径:指的是ip:端口后的路径。(如127.0.0.1:8080/home中的/home是路径)
- 什么是路由?
(1) 一个路由就是一个映射关系(key:value)
(2) key为路径, value可能是function或component
- 路由分类
(1) 后端路由:
a. 理解: value是function, 用来处理客户端提交的请求。
b. 注册路由: router.get(path, function(req, res))
c. 工作过程:当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
(2) 前端路由:
a. 浏览器端路由,value是component,用于展示页面内容。
b. 注册路由:
c. 工作过程:当浏览器的path变为/test时, 当前路由组件就会变为Test组件
前端路由补充:
1. 前端路由靠BOM(BrowserObjectModel浏览器对象模型)的history属性(浏览器的历史记录)实现。
2. 存储BOM的history属性的容器是一个先入后出的栈
3. 操作history属性有两种方法
(1)let history = History.createBrowserHistory()
直接使用H5推出的history身上的API,旧浏览器不兼容
(2)let history = History.createHashHistory()
使用hash值(锚点跳转),兼容性好,但是不推荐(丑)
路由器进行调用的组件称为路由组件。
一般组件 | 路由组件 | |
---|---|---|
写法不同 | ||
存放位置不同 | src/components | src/pages |
接收到的props不同 | 写组件标签时传递了什么,就能收到什么 | 接收到路由器发送的三个固定的属性,如下 |
路由组件接收的props
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: “/about”
search: “”
state: undefined
match:
params: {}
path: “/about”
url: “/about”
点击About或Home会跳转路由(一级路由)
点击Home下的Nerws和Message跳转路由(二级路由)
点击Message中信息跳转路由(三级路由)
示例:src/index.js(引入路由器的一般写法)
// 引入react核心库 import React from 'react' // 引入ReactDOM import ReactDOM from 'react-dom' // 引入路由器 import {BrowserRouter} from 'react-router-dom' // 引入App import App from './App' ReactDOM.render( <BrowserRouter> <App/> </BrowserRouter>, document.getElementById('root') )
BrowserRouter常用,HashRouter兼容好
BrowserRouter | HashRouter | |
---|---|---|
底层原理 | BrowserRouter使用的是H5的history API(不是this.props.history), 不兼容IE9及以下版本。 |
HashRouter使用的是URL的哈希值, 会把/#后的字符串转化为哈希值。 |
path表现形式 | BrowserRouter的路径中没有#, 例如: localhost:3000/demo/test |
HashRouter的路径包含#, 例如: localhost:3000/#/demo/test |
刷新后对路由state参数的影响 | BrowserRouter没有任何影响,因为state保存在history对象中。 | HashRouter刷新后会导致路由state参数的丢失!!! |
备注:HashRouter直接调用前端资源,不会向后端发送请求;可以用于解决一些路径错误相关的问题。
Demo
,会把path改为localhost:3000/a
,会匹配路径/a
,展示Demo组件public/index.html:略
src/App.jsx(不带样式)
import React, { Component } from 'react' import {Link,Route} from 'react-router-dom' import Home from './pages/Home' import About from './pages/About' export default class App extends Component { render() { return ( <div> <h2>React Router Demo</h2> {/* 在React中靠路由链接实现切换组件--编写路由链接 */} <Link className="list-group-item" to="/about">About</Link> <Link className="list-group-item" to="/home">Home</Link> {/* 注册路由 */} <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </div> ) } }
src/pages/Home/index.jsx
import React, { Component } from 'react' export default class Home extends Component { render() { return ( <h3>我是Home的内容</h3> ) } }
src/pages/About/index.jsx
import React, { Component } from 'react' export default class About extends Component { render() { return ( <h3>我是About的内容</h3> ) } }
将
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
改为
<NavLink className="list-group-item" to="/about">About</Link>
<NavLink className="list-group-item" to="/home">Home</Link>
可以添加选中高亮效果。
可以在选中的className后默认追加
active
表示高亮,同时bootstrap.css的样式认为有active的className就是高亮,所以这里可以显示正确。如果使用的是其他的样式库则不一定。的
activeClassName
可以指定追加的className后缀
所以
<NavLink className="list-group-item" to="/about">About</Link>
<NavLink className="list-group-item" to="/home">Home</Link>
等价于
<NavLink activeClassName="active" className="list-group-item" to="/about">About</Link>
<NavLink activeClassName="active" className="list-group-item" to="/home">Home</Link>
每次用
都要传activeClassName
和className
属性太麻烦,可以自己封装好这两个属性
src/App.jsx(不带样式)
import React, { Component } from 'react' import {Route} from 'react-router-dom' import Home from './pages/Home' import About from './pages/About' import MyNavLink from './components/MyNavLink' export default class App extends Component { render() { return ( <div> <h2>React Router Demo</h2> {/* 在React中靠路由链接实现切换组件--编写路由链接 */} <MyNavLink to="/about">About</MyNavLink> <MyNavLink to="/home">Home</MyNavLink> {/* 注册路由 */} <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </div> ) } }
src/components/MyNavLink/index.jsx
import React, { Component } from 'react' import {NavLink} from 'react-router-dom' export default class MyNavLink extends Component { render() { return ( <NavLink activeClassName="active" className="list-group-item" {...this.props}/> ) } }
为什么使用
About 就可以完全赋值?
About是作为标签体内容(<标签名 标签属性>标签体标签名>
)传入,标签体是一种特殊的标签属性,标签属性固定为children。
总结:
- NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
- 标签体内容是一个特殊的标签属性(children)
- 通过this.props.children可以获取组件标签的标签体内容
现象:
- 当
写成
About ,且
About 写成
后,若在跳转后刷新页面,则可能造成样式丢失。
原因:
- 初始化时查找的是localhost:3000/css/bootstrap.css,显示正确;
跳转多级页面刷新后查找的是localhost:3000/abc/css/bootstrap.css,找不到资源时会显示public/index.html,所以找到了public/index.html,虽然状态码是200,但不是css样式,而是html文件。
解决:
- 【常用】public/index.html中的
改为(去掉路径里的点)
- 【常用】【React专属】public/index.html中的
改为(路径前加“%PUBLIC_URL%”)
- 【不常用】在App.jsx中使用
现象:
src/App.jsx
... <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Route path="/home" component={Test}/> ...
问题:
解决:
标签即可src/App.jsx
... <Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Route path="/home" component={Test}/> </Switch> ...
一般写在所有路由注册的最下方,当路由都没有命中时,跳转到
指定的路由。
示例:默认展示
/about
... <Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Redirect to="/about"/> </Switch> ...
的path属性值是
的to属性值从头开始的(完全包含path属性值的)子串 to="?" |
|
是否匹配 |
---|---|---|
/home | /home | 是 |
/home/a/b | /home | 是 |
/home | /home/a/b | 否 |
/a/home/b | /home | 否 |
严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
标签加上exact={true}
(简写exact
)
to="?" |
|
是否匹配 |
---|---|---|
/home | /home | 是 |
/home/a/b | /home | 否 |
/home | /home/a/b | 否 |
/a/home/b | /home | 否 |
src/App.jsx(不能开启精确模式)(不带样式)
import React, { Component } from 'react' import {Link,Route} from 'react-router-dom' import Home from './pages/Home' import About from './pages/About' export default class App extends Component { render() { return ( <div> <h2>React Router Demo</h2> {/* 在React中靠路由链接实现切换组件--编写路由链接 */} <Link className="list-group-item" to="/about">About</Link> <Link className="list-group-item" to="/home">Home</Link> {/* 注册路由 */} <Route path="/about" component={About}/> <Route path="/home" component={Home}/> </div> ) } }
src/pages/Home/index.jsx
import React, { Component } from 'react' import MyNavLink from '../../components/MyNavLink' import {Route,Switch,Redirect} from 'react-router-dom' 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> ) }
src/pages/Home/Message/index.jsx
import React, { Component } from 'react' export default class Message extends Component { render() { return ( <div> <ul> <li> <a href="/message1">message001</a> </li> <li> <a href="/message2">message002</a> </li> <li> <a href="/message/3">message003</a> </li> </ul> </div> ) } }
src/pages/Home/News/index.jsx
import React, { Component } from 'react' export default class News extends Component { render() { return ( <ul> <li>news001</li> <li>news002</li> <li>news003</li> </ul> ) } }
一级路由跳转Home,二级路由跳转Message,三级路由跳转Detail
下面用法中的代码都是演示传递id和titile两个属性
类似Ajax中传递使用的params参数
父组件:
...
{/* 向路由组件传递params参数(携带参数) */}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
...
{/* 声明接收params参数(声明接收) */}
<Route path="/home/message/detail/:id/:title" component={Detail}/>
...
子组件:
...
// 接收params参数
const {id,title} = this.props.match.params
...
tips:
- 子路由组件接收的参数从
this.props.match.params
中取中的
:id
和:title
是属性的key
类似Ajax中的query参数
父组件:
...
{/* 向路由组件传递search参数(携带参数) */}
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
...
{/* search参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
...
子组件:
import qs from 'querystring'
...
// 接收search参数
// qs.parse: 将urlencode格式转化为json格式
// search.slice(1): 去掉第一个"?"字符
const {search} = this.props.location
const {id,title} = qs.parse(search.slice(1))
...
tips:
- 子路由组件接收的参数从
this.props.location.search
中取- 获取到的search是urlencoded编码字符串,需要借助querystring解析
类似Ajax中的query参数
父组件:
...
{/* 向路由组件传递state参数(携带参数) */}
<Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>
...
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
...
子组件:
import qs from 'querystring'
...
// 接收search参数
const {id,title} = this.props.location.state || {}
...
tips:
- 子路由组件接收的参数从
this.props.location.state
中取- 刷新也可以保留住参数(因为用的是
BrowserRouter
,会自动记录参数)
src/pages/Home/Message/index.jsx
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参数 */} {/* {msgObj.title} */} {/* 向路由组件传递search参数 */} {/* {msgObj.title} */} {/* 向路由组件传递state参数 */} <Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link> </li> ) }) } </ul> <hr/> {/* 声明接收params参数 */} {/*
*/ } {/* search参数无需声明接收,正常注册路由即可 */} {/**/ } {/* state参数无需声明接收,正常注册路由即可 */} <Route path="/home/message/detail" component={Detail}/> </div> ) } }
src/pages/Home/Message/Detail/index.jsx
import React, { Component } from 'react' import qs from 'querystring' const DetailData = [ {id:'01',content:'01内容'}, {id:'02',content:'02内容'}, {id:'03',content:'03内容'} ] export default class Detail extends Component { render() { console.log(this.props); // 接收params参数 // const {id,title} = this.props.match.params // 接收search参数 // const {search} = this.props.location // const {id,title} = qs.parse(search.slice(1)) // 接收state参数 const {id,title} = this.props.location.state || {} const findResult = DetailData.find((detailObj)=>{ return detailObj.id === id }) || {} return ( <ul> <li>ID:{id}</li> <li>TITLE:{title}</li> <li>CONTENT:{findResult.content}</li> </ul> ) } }
push模式下 路由产生的新页面 会作为一个新结点 push进历史记录栈
例子:
- 先后进入:
localhost:3000
、localhost:3000/home
、localhost:3000/home/massage
、localhost:3000/home/massage/1
- 栈中的数据顺序(自底向上):
localhost:3000
、localhost:3000/home
、localhost:3000/home/massage
、localhost:3000/home/massage/1
- 出栈的顺序就是
localhost:3000/home/massage/1
、localhost:3000/home/massage
、localhost:3000/home
、localhost:3000
replace模式下 路由产生的新页面 会作为一个新结点 替换历史记录栈中存在的最新的结点
开启:
简写为:
例子:在/home/massage/index.jsx中配置replace的情况
- 先后进入:
localhost:3000
、localhost:3000/home
、localhost:3000/home/massage
、localhost:3000/home/massage/1
- 栈中的数据顺序(自底向上):
localhost:3000
、localhost:3000/home
、localhost:3000/home/massage/1
- 出栈的顺序就是
localhost:3000/home/massage/1
、localhost:3000/home
、localhost:3000
解释:所有massage的子组件的跳转在历史记录栈中都会替换掉最新的
借助this.prosp.history对象上的API对操作路由跳转、前进、后退
方法 | 参数 | 作用 |
---|---|---|
this.prosp.history.push() | path路由路径, state状态参数 | push方法跳转path指定的页面 |
this.prosp.history.replace() | path路由路径, state状态参数 | replace方法跳转path指定的页面 |
this.prosp.history.goBack() | 无参数 | 回退到上一页面 |
this.prosp.history.goForward() | 无参数 | 前进到下一页面 |
this.prosp.history.go() | n步长 | n>0, 前进n个页面; n<0, 后退n个页面; n=0, 无变化 |
src/pages/Home/Message/index.jsx
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'}, ] } replaceShow = (id,title)=>{ //replace跳转+携带params参数 //this.props.history.replace(`/home/message/detail/${id}/${title}`) //replace跳转+携带search参数 // this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`) //replace跳转+携带state参数 this.props.history.replace(`/home/message/detail`,{id,title}) } pushShow = (id,title)=>{ //push跳转+携带params参数 // this.props.history.push(`/home/message/detail/${id}/${title}`) //push跳转+携带search参数 // this.props.history.push(`/home/message/detail?id=${id}&title=${title}`) //push跳转+携带state参数 this.props.history.push(`/home/message/detail`,{id,title}) } back = ()=>{ this.props.history.goBack() } forward = ()=>{ this.props.history.goForward() } go = ()=>{ this.props.history.go(-2) } render() { const {messageArr} = this.state return ( <div> <ul> { messageArr.map((msgObj)=>{ return ( <li key={msgObj.id}> {/* 向路由组件传递params参数 */} {/* {msgObj.title} */} {/* 向路由组件传递search参数 */} {/* {msgObj.title} */} {/* 向路由组件传递state参数 */} <Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title: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> </li> ) }) } </ul> <hr/> {/* 声明接收params参数 */} {/*
*/ } {/* search参数无需声明接收,正常注册路由即可 */} {/**/ } {/* state参数无需声明接收,正常注册路由即可 */} <Route path="/home/message/detail" component={Detail}/> <button onClick={this.back}>回退</button> <button onClick={this.forward}>前进</button> <button onClick={this.go}>go</button> </div> ) } }
withRouter可以加工一般组件,让一般组件具备路由组件所特有的API
withRouter的 返回值 是一个新组件
src/components/Header/index.jsx:主要看最后一行,使用withRouter暴露组件Header
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() } go = ()=>{ this.props.history.go(-2) } render() { console.log('Header组件收到的props是',this.props); return ( <div className="page-header"> <h2>React Router Demo</h2> <button onClick={this.back}>回退</button> <button onClick={this.forward}>前进</button> <button onClick={this.go}>go</button> </div> ) } } export default withRouter(Header)
组件库是别人写好的包含样式的组件
ant-design(国内蚂蚁金服,简称antd)
(1) 官网: https://ant.design/index-cn
(2) Github: https://github.com/ant-design/ant-design/
(3) 安装:npm i install antd --save
material-ui(国外)
(1) 官网: http://www.material-ui.com/#/
(2) Github: https://github.com/callemall/material-ui
element ui (国内饿了么)
(1) 官网:https://element.eleme.cn/#/zh-CN
(2) Github:https://github.com/ElemeFE/element
(3) 安装:npm i element-react --save
vant ui(国内有赞,手机端比较好看)
(1) 官网:https://vant-contrib.gitee.io/vant/#/zh-CN/
(2) Github:https://github.com/mxdi9i7/vant-react
(3) Gitee:https://gitee.com/vant-contrib/vant#https://github.com/mxdi9i7/vant-react
(4) 安装:npm i vant-react --save
官网使用方法(因为官网的文档肯定比这里随便列的全):[视频链接]
一般使用要在文件头加入import 'antd/dist/antd.css'
,这样会把所有的样式都引入,有很多不需要的样式
官网对应链接:https://ant.design/docs/react/use-with-create-react-app-cn
npm install react-app-rewired customize-cra babel-plugin-import less less-loader --save
....
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
....
//配置具体的修改规则
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
// 按需导入
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
// 修改less主体颜色
addLessLoader({
lessOptions: { // 操作less
javascriptEnabled: true, // 允许用js修改底层less文件
modifyVars: { '@primary-color': 'blue' }, // 修改变量: { '主色调': '蓝色' }(颜色可以使用单词和RGB)
}
}),
);
import 'antd/dist/antd.css'
应该删掉npm install @craco/craco babel-plugin-import less less-loader --save
....
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
....
const CracoLessPlugin = require('craco-less');
module.exports = {
babel: {
plugins: [
['import', // 按需引入样式。插件名babel-plugin-import说明了在配置中的位置是babel、plugins、import
{
libraryName: 'antd', // 针对antd库
libraryDirectory: "es", // 使用es模块化规范
style: true // 针对less生成的css文件进行按需引入(true比"css"用途更广一些)
}]
]
},
plugins: [
{
plugin: CracoLessPlugin, // craco的修改.less文件的插件(修改主题颜色)
options: {
lessLoaderOptions: { // .less文件操作加载器
lessOptions: { // 操作.less文件
modifyVars: {'@primary-color': '#00f'}, // 修改变量: { '主色调': '蓝色' }(颜色可以使用单词和RGB)
javascriptEnabled: true, // 允许使用js修改.less文件
}
}
}
}]
};
import 'antd/dist/antd.css'
应该删掉