跟着尚硅谷的天禹老师学习React
看视频可以直接点击 b站视频地址
SPA的理解
- 单页Web应用(Single Page Application)
- 整个应用只有一个完整的页面
简单来讲就是只有一个html文件,比如React脚手架搭建后只有一个放在public文件夹的index.html - 点击页面中的链接只会做部分界面的更新。
- 数据都需要通过ajax请求获取,并在前端异步更新
路由的理解
可以理解在页面组织过程中,以路径作为键,以组件作为值得映射过程。
- 什么是路由
- 一个路由就是一个映射关系
- key为路径,value可能是function或component
- 路由分类
- 后端路由
- 理解:value是function,用于处理客户端提交的请求
- 注册路由:router.get(path,function(req,res))
- 工作过程:当node接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据。
- 后端路由
- 浏览器端路由,value是component,用于展示页面内容
- 注册路由:<Route path="/test" component=[Test]>
- 工作过程:当前端的一些操作使得浏览器的path变为‘/test’时,当前路由组件就会变成Test组件
- 后端路由
history
history:
- 本身是一个栈结构
- History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录
- 是BOM提供的能力
代码
<!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.createBrowserHistory() //方法一,直接使用H5推出的history身上的API
let history = History.createHashHistory() //方法二,hash值(锚点)
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>
history的两种模式
- hash
hash型的特点是url中会有一个“#”号,似乎也是vue脚手架默认的形式,对应的api为History.createHashHistory,有点类似锚点跳转,点击不会刷新页面,但会在浏览记录中留下记录。兼容性好。 - browser
没有锚点的就是这种类型,是html5新提供的能力。对应的api为History.createBrowserHistory
react-router
注意这里学习的是react-router-dom的5版本,目前react-router-dom已经更新至6版本,安装时应该使用“@”符号来标注要安装的版本 :
(npm install)/(yarn add) react-router-dom@5
react-router的理解
- 一个react插件库
- 专门用来实现一个SPA应用
- 基于react的项目几乎都会用到这个库
- 有三个环境:web、native、any
- 前端学的是react-router-dom
基本用法
代码
写好Home和About组件后引入,使用Link标识跳转路径,使用Route注册路径。要注意无论是Link还是Route都要放在Router(例如BrowserRouter)中,这里有个取巧的办法,直接在App组件外包裹一层即可。
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
import React, { Component } from "react";
import { Link, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
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">
{/* 在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>
);
}
}
路由组件与一般组件
1、定义
一般组件就是正常定义的并使用的组件
<Home/>
路由组件就是通过路由匹配来渲染的组件
<Route path="/home" component={Home}>
2、使用规范
如果是路由组件,需要单独在根目录下创建一个名为pages的目录,并将路由组件放在其中;一般组件需要放在components文件夹下。
3、最大的区别
除了定义上的区别外,一般组件收到的props是在引用时传入的,路由组件会收到路由器传入的history、location、match三个属性。
小结
- 明确好界面中的导航区、展示区
- 导航区的a标签改为Link标签(使用to属性来标识跳转地址)
<Link to="/demo">Demo</Link>
- 展示区写Route标签进行路由匹配(使用path属性来匹配路径,使用component属性来配置展示的组件)
<Route path="/demo" component={Demo}></Route>
- App外侧包裹<BrowserRouter>或者<HashRouter>
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
- HashRouter会使url中出现锚点(#),关于“#”符号,如果是后端来处理,不会获取任何资源,对于后端没有任何价值。
- 路由组件和一般组件的区别
- 写法不同
- 存放位置不同
- 接收到的props不同:
- 一般组件:写组件标签时传入什么就能接收什么
- 接收到了三个固定的属性location、match、history(history.location和location是一个东西)
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"
NavLink的使用
可以自动实现高亮效果。NavLink可以自动将当前路由的link的class添加一个active类名,或者指定activeClassName后,当为本路由时,展示这个class效果。这里有高亮效果是因为bootstrap本身就有一个active的类,恰好成了高亮。
<NavLink activeClassName="demo" className="list-group-item" to="/about">About</NavLink>
<NavLink activeClassName="demo" className="list-group-item" to="/home">Home</NavLink>
如果要在引入bootstrap的情况下想要覆盖其样式,最好加一个“!important”,因为bootstrap的选择器权重比较高。
封装NavLink
如果打印this.props会发现,MyNavLink还接收了一个名为children的属性,会发现这个children就是我们包裹在MyNavLink中的元素,在MyNavLink中只需要使用{…this,props}即可传入所有props,其中也包括children这一个值,所以等价于在NavLink中指定了子元素(这有些 类似Vue的slot思想)。
所以如果需要在NavLink组件中展示类似插槽的效果时,只需要在NavLink中指定children的值即可,或者更简单的是直接在NavLink中传入一个{…this,props}。
<MyNavLink activeClassName="active1" className="list- group-item" to="/about">About</MyNavLink>
import React, { Component } from "react";
import { NavLink } from "react-router-dom";
export default class MyNavLink extends Component {
render() {
return (
<div>
<NavLink
activeClassName="active1"
className="list-group-item"
{...this.props}
/>
</div>
);
}
}
小结
- NavLink可以实现路由链接的高亮,通过activeClassName指定样式名
- 标签体内是一个特殊的标签属性
- 通过this.props.children可以获取标签体内容
继续匹配问题
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Route path="/home" component={Test} />
上面代码会导致当路径匹配到home时,会同时显示Home和Test组件
Switch组件
Swtich组件可以解决上述问题。
Switch组件会优先匹配第一个组件。
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Route path="/home" component={Test} />
</Switch>
- 通常情况下,path和component时一一对应的关系。
- Swtich可以提高路由匹配效率(单一匹配)
样式丢失问题
localhost:3000是脚手架起的服务,由devServer创建。
public文件夹下可以对外提供资源服务。
当请求了不存在的资源,将回传index.html文件。
上面的bootstrap.css可以通过http://localhost:3000/css/bootstrap.css访问
当我们使用以下方式(相对路径)来引入css文件时
<link href="./css/bootstrap.css" rel="stylesheet"/>
在访问localhost:3000时,我们的样式是正确的,但是当我们切换一下路由(localhost:3000/site/home,这里其实是加了一个前缀/site),刷新页面后,样式丢失了。
这是因为我们使用的是相对路径,当此时路由为localhost:3000/site时,由于相对路径的原因,css地址为localhost:3000/site/css/bootstrap.css(而非预料中的localhost:3000/css/bootstrap.css),没有找到这个资源,在我们配置了setUpProxy的前提下转发到了5000端口上(即使不转发如果请求的是前端服务就会返回index.html),因此样式丢失了。
我们可以这样写:
<link href="/css/bootstrap.css" rel="stylesheet" />
或者绝对路径
<link href="http://localhost:3000/css/bootstrap.css" rel="stylesheet" />
或者使用"%PUBLIC_URL%"
<link href="%PUBLIC_URL%/css/bootstrap.css" rel="stylesheet" />
或者使用HashRouter,因为Hash路由并不会将“#”后面的URL带给服务器。这样link标签中还是可以写成(多了一个点)
<link href="./css/bootstrap.css" rel="stylesheet" />
yarn和npm混用
很容易造成依赖包的丢失
严格匹配与模糊匹配
和Vue一样,启用模糊匹配(默认)时:
<MyNavLink activeClassName="active1" className="list-group-item" to="/home/a">
<Route path="/home" component={Home} />
可以匹配
<MyNavLink activeClassName="active1" className="list-group-item" to="/a/home">
<Route path="/home" component={Home} />
不可以匹配
<MyNavLink activeClassName="active1" className="list-group-item" to="/home">
<Route path="/home/a" component={Home} />
不可以匹配
可以这样理解:
只要Route需要的,NavLink都必须给全。
如果不希望第一种情况被匹配到,就需要开启严格匹配,但一般不推荐严格匹配:
<Route exact={true} path="/home/a" component={Home} />
或者
<Route exact path="/home/a" component={Home} />
小结
- 默认开启的时模糊匹配,规则简记:【输入的路径】必须要包含【匹配的路径】,且顺序要一致
- 开启严格匹配需要用到在Route中加入指明属性
- 严格匹配要慎重开启,需要的时候再开,有些时候开启会导致无法继续匹配二级路由。
Redirect的使用
Redirect放在Route的下面,当上面的Route都没有匹配时,就要重定向至指定的路径。
下方代码,当about和home都没有被匹配到时,重定向到/about路径。
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Redirect to="/about"/>
</Switch>
嵌套路由
和一级路由没有什么太大区别,只是注意一下需要在Route的path属性上和NavLink的to属性上要加上一级路由的路径,同时注意不要在一级路由组件上开启严格匹配。
import React, { Component } from "react";
import MyNavLink from "../../components/MyNavLink";
import { Route, Switch } from "react-router-dom";
import News from "./News";
import Message from "./Message";
export default class Home extends Component {
render() {
return (
<div>
<h1>Home</h1>
<div>
<ul className="nav nav-tabs">
<li>
<MyNavLink to="/home/news" className="list-group-item">
News
</MyNavLink>
</li>
<li>
<MyNavLink to="/home/message" className="list-group-item">
Message
</MyNavLink>
</li>
</ul>
</div>
{/* 注册路由 */}
<Switch>
<Route path="/home/message" component={Message} />
<Route path="/home/news" component={News} />
</Switch>
</div>
);
}
}
注意
- 注册子路由时要写上父路由的path值
- 路由的匹配是按照注册路由的顺序进行的
向路由组件传递参数
携带params参数
代码
Detail.jsx
/* Detail.jsx */
import React, { Component } from "react";
export default class Detail extends Component {
render() {
const { id, title, content } = this.props.match.params; // 接收params参数
return (
<div>
<ul>
<li>id:{id}</li>
<li>title:{title}</li>
<li>content:{content}</li>
</ul>
</div>
);
}
}
Message.jsx
/* Message.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: 1, title: "消息1", content: "我爱你中国1" },
{ id: 2, title: "消息2", content: "我爱你中国2" },
{ id: 3, title: "消息3", content: "我爱你中国3" },
],
};
render() {
const { messageArr } = this.state;
return (
<div>
<ul>
{messageArr.map((msg) => {
return (
<li key={msg.id}>
<Link to={`/home/message/detail/${msg.id}/${msg.title}/${msg.content}`}>{msg.title}</Link>
</li>
);
})}
</ul>
<Route
path="/home/message/detail/:id/:title/:content"
component={Detail}
></Route>
<hr />
</div>
);
}
}
- params参数
- 路由链接(携带参数):<Link to="/demo/test/tom/.18">
- 注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Demo} />
- 接收参数:const (name, age) = this.props.match.params
传递search参数
类似向后端发请求时url的query参数。
urlencoded编码格式
key1=value1&key1=value1
querystring库
是一个node自带的库,现在叫qs库
用法类似JSON这个对象
import qs from 'qs'
let obj = {name:"tom",age:18}
const encodedurl = qs.stringify(obj) // name=tom&age=18
const decodedurl = qs.parse(encodedurl) // {name:"tom",age:"18"}
代码
Detail.jsx
/* Detail.jsx */
import React, { Component } from "react";
import qs from 'qs'
export default class Detail extends Component {
render() {
const { search } = this.props.location
const {id,title,content} = qs.parse(search.slice(1))
return (
<div>
<ul>
<li>id:{id}</li>
<li>title:{title}</li>
<li>content:{content}</li>
</ul>
</div>
);
}
}
Message.jsx
/* Message.jsx */
import React, { Component } from "react";
import { Link, Route } from "react-router-dom";
import Detail from "./Detail";
import qs from "qs";
export default class Message extends Component {
state = {
messageArr: [
{ id: 1, title: "消息1", content: "我爱你中国1" },
{ id: 2, title: "消息2", content: "我爱你中国2" },
{ id: 3, title: "消息3", content: "我爱你中国3" },
],
};
render() {
const { messageArr } = this.state;
console.log(qs)
console.log(qs.stringify({ name: 1 }));
return (
<div>
<ul>
{messageArr.map((msg) => {
return (
<li key={msg.id}>
<Link to={`/home/message/detail?${qs.stringify(msg)}`}>
{msg.title}
</Link>
</li>
);
})}
</ul>
<Route path="/home/message/detail" component={Detail}></Route>
<hr />
</div>
);
}
- search参数
- 路由链接(携带参数):<Link to="/demo/test?name=tom&age=18">Demo</Link>
- 注册路由:无需特殊处理,正常注册即可
- 接收参数:const = { search } = this.props.location
- 备注:获取到的search是urlencoded编码字符串,传参和转换参数时可以借助querystring这个库来编码和解析。
传递state参数
代码
Detail.jsx
/* Detail.jsx */
import React, { Component } from "react";
export default class Detail extends Component {
render() {
const { id, title, content } = this.props.location.state || {};
return (
<div>
<ul>
<li>id:{id}</li>
<li>title:{title}</li>
<li>content:{content}</li>
</ul>
</div>
);
}
}
Message.jsx
/* Message.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: 1, title: "消息1", content: "我爱你中国1" },
{ id: 2, title: "消息2", content: "我爱你中国2" },
{ id: 3, title: "消息3", content: "我爱你中国3" },
],
};
render() {
const { messageArr } = this.state;
return (
<div>
<ul>
{messageArr.map((msg) => {
return (
<li key={msg.id}>
<Link
to={{
pathname: "/home/message/detail",
state: { id: msg.id, title: msg.title },
}}
>
{msg.title}
</Link>
</li>
);
})}
</ul>
<Route path="/home/message/detail" component={Detail}></Route>
<hr />
</div>
);
}
}
- state参数
- 路由链接(携带参数):<Link to={pathname:’/demo/test’,state:{name:‘tom’,age:18}}>Demo</Link>
- 注册路由:无需声明,正常注册即可
- 接收参数:this.props.location.state
- 备注:当前刷新可以保留住参数,to属性只能写成对象形式
总结
- params参数使用最多,search参数用的一般,state参数一般用于不想展示数据到地址栏里。
- params参数、search参数,也可以写成对象形式
push与replace
push是一个压栈操作,replace是替换掉当前地址。
当前页面就是history栈栈顶的内容。
前面的例子都是使用push来做的,如果需要使用replace,只需要在Link类组件(Link、NavLink等都可以)中将replace设置为true即可。
编程式路由导航
push和replace
// 因为要给事件传参所以要用高阶函数
replaceShow = (id, title, content) => {
return () => {
// params
this.props.history.replace(
`/home/message/detail/${id}/${title}/${content}`
);
// search
this.props.history.replace(
`/home/message/detail?${qs.stringify({id,title,content})}`
);
// state
this.props.history.replace(
`/home/message/detail`,{id,title,content}
);
};
};
pushShow = (id, title, content) => {
return () => {
// params
this.props.history.push(
`/home/message/detail/${id}/${title}/${content}`
);
// search
this.props.history.push(
`/home/message/detail?${qs.stringify({id,title,content})}`
);
// state
this.props.history.push(
`/home/message/detail`,{id,title,content}
);
};
go、goForward、go
back = () => {
this.props.history.goBack();
};
forward = () => {
this.props.history.goForward();
};
// 因为要给事件传参所以要用高阶函数
jump = (n) => {
return () => {
this.props.history.go(n);
};
};
总结
实际上就是借用了this.props.history对象上的API操作路由跳转、前进、后退
- this.props.history.push()
- this.props.history.replace()
- this.props.history.goBack()
- this.props.history.goForward()
- this.props.history.go()
withRouter
- withRouter是react-router-dom中的一个方法,用于加工一般组件变成路由组件,这样就可以在一般组件中也拿到路由组件特有的API(比如history)。
- withRouter的返回值是一个新组件。
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
class Header extends Component {
back = () => {
// 只有路由组件才由history对象
// this.props.history.goBack();
};
forward = () => {
this.props.history.goForward();
};
jump = (n) => {
return () => {
this.props.history.go(n);
};
};
render() {
return (
<div className="page-header">
<h2>React Router Demo</h2>
<button onClick={this.back}>back</button>
<button onClick={this.forward}>forward</button>
<button onClick={this.jump(-1)}>jump</button>
</div>
);
}
}
export default withRouter(Header)
BrowserRouter和HashRouter
- 底层原理不一样
- BrowserRouter使用的是H5的history API,不兼容IE9以下的版本。
- HashRouter使用的URL的哈希值。
- url表现形式不一样
- BrowserRouter的路径中没有#
- 刷新后对路由state参数的影响
- BrowserRouter没有任何影响
- HashRouter刷新后会导致state参数的丢失
- 备注:HashRouter可以用于解决一下路径错误相关的问题。