随着ajax的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是单页Web应用(Single Page Web Application,SPA)的兴起,前端路由系统随之开始流行。
所谓单页Web应用,就是只有一张Web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的Web应用程序。浏览器一开始会加载必须的HTML、CSS和Javascript,所有的操作都在这张页面上完成,都由Javascript来控制。
ajax更新页面状态的情况下):
url,再次打开该url时,网页还是保存(分享)时的状态);ajax更新页面之前的状态,url也回到之前的状态);url且不让浏览器向服务器发出请求;url的变化;url地址,并解析出需要的信息来匹配路由规则。我们路由常用的hash模式和history模式实际上就是实现了上面的功能。
hash模式是一种把前端路由的路径用井号#拼接在真实url后面的模式。当井号#后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发onhashchange事件。
这里的hash就是指 url 尾巴后的#号以及后面的字符。这里的#和css里的#是一个意思。hash也称作锚点,本身是用来做页面定位的,它可以使对应id的元素显示在可视区域内。
由于hash值变化不会导致浏览器向服务器发出请求,而且hash改变会触发hashchange事件,浏览器的进后退也能对其进行控制,所以人们在html5的history出现前,基本都是使用hash来实现前端路由的。
缺点:传递参数的方式是在url后拼接,会有体积的限制
使用到的api:
window.location.hash = 'qq' // 设置 url 的 hash,会在当前url后加上 '#qq'
var hash = window.location.hash // '#qq'
window.addEventListener('hashchange', function () {
// 监听hash变化,点击浏览器的前进后退会触发
})
history API是H5提供的新特性,允许开发者直接更改前端路由,即更新浏览器URL地址而不重新发起请求,它通过监听window的popstate来实现的。
history路由特点:
url可以是与当前url同源的任意url,也可以是与当前url一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中。history.state,添加任意类型的数据到记录中。title属性,以便后续使用。pushState、replaceState来实现无刷新跳转的功能。back、forward、go)对于history来说,确实解决了不少hash存在的问题,但是也带来了新的问题。具体如下:
history模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果nginx没有匹配得到当前的url,就会出现404的页面。hash模式来说, 它虽然看着是改变了url,但不会被包括在http请求中。所以,它算是被用来指导浏览器的动作,并不影响服务器端。因此,改变hash并没有真正地改变url ,所以页面路径还是之前的路径,nginx也就不会拦截。history模式时,需要通过服务端来允许地址可访问,如果没有设置,就很容易导致出现404的局面。
history模式与hash模式的比较:
1、hash本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。
2、hash的传参是基于url的,如果要传递复杂的数据,会有体积的限制,而history模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。
相关API如下表:
| API | 说明 |
|---|---|
window.history.pushState(state, title, url) |
pushState主要用于往历史记录堆栈顶部添加一条记录。state:需要保存的数据,这个数据在触发popstate事件时作为参数传递过去title:页面标题,基本没用,一般传null,当前所有浏览器都会忽略此参数url:设定新的历史记录的url,缺少时表示为当前页地址。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以是绝对路径,也可以是相对路径。 |
window.history.replaceState(state, title, url) |
更改当前的历史记录,与pushState参数相同 |
window.history.state |
用于存储以上方法的data数据,不同的浏览器读写权限不一样 |
window.onpopstate |
监听浏览器前进后退事件,pushState与replaceState方法不会触发 |
window.history.back() |
后退 |
window.history.forward() |
前进 |
window.history.go(1) |
前进一步,-2为后退两步 |
window.history.length |
查看当前历史堆栈中页面的数量 |
示例:当前url是 https://www.baidu.com/a/,
①执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/;
②执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
npm install react-router-dom
yarn add react-router-dom
以上方式默认安装最新版本,react-router-dom的两个版本,5和6的使用方式不同,根据需要安装。
HashRouter和BrowserRouter决定了路由模式分别是hash模式和history模式,并且这两个组件是路由的容器,必须放在最外层。
1、hash模式
ReactDOM.render(
<HashRouter>
<Route path="/" component={Home}/>
HashRouter>
)
2、history模式
ReactDOM.render(
<BrowserRouter>
<Route path="/" component={Home}/>
BrowserRouter>
)
Route实现了路径和显示组件之间的映射。
V5写法:
<Route path="/路径" component={组件} render={返回dom} location="route对象" exact="匹配规则"/>
它的参数说明如下:
| 参数 | 说明 |
|---|---|
| path | 跳转的路径 |
| component | 对应路径显示的组件 |
| render | 可以自己写render函数返回具体的dom,而不需要去设置component |
| location | 传递route对象,和当前的route对象对比,如果匹配则跳转 |
| exact | 匹配规则,默认值为false,true的时候则精确匹配。 |
V6写法:
<Route path="/路径" element={组件} render={返回dom} location="route对象"/>
示例:
<Routes>
<home/>} />
<About/>} />
<Topics/>} />
Routes>
注意:V6中不需要exact属性,它默认就是匹配完整路径。
Router组件是底层路由,可以管理路由的状态,可以代替HashRouter和BrowserRouter。
下面与作用相同:
<Router history={history}>
// ...
Router>
下面与作用相同:
<Router history={createHashHistory()}>
// ...
Router>
1、Link组件的api:
(1)to:目标页面的路径,两种写法,表示跳转到哪个路由
// 字符串写法
<Link to="/a" />
// 对象写法
<Link to={{
pathname: '/courses',
search: '?sort=name',
hash: '#the-hash',
state: {fromDashboard: true}
}}/>
(2)replace:就是将push改成replace
(3)innerRef:访问Link标签的dom
2、NavLink组件的api属性:包含了Link组件的所有api,在Link组件的基础上进行了扩展
(1)Link的所有api
(2)activeClassName:路由激活的时候设置的类名
(3)activeStyle:路由激活设置的样式
(4)exact:参考Route,符合这个条件才会激活active类
(5)strict:参考Route,符合这个条件才会激活active类
(6)isActive:接收一个回调函数,active状态变化的时候回触发,返回false则中断跳转
示例代码:
const oddEvent = (match, location) => {
if (!match) {
return false
}
console.log(match.id)
return true
}
<NavLink isActive={oddEvent} to="/a/123">组件一NavLink>
(7)location:接收一个location对象,当url满足这个对象的条件才会跳转
<NavLink to="/a/123" location={{ key:"mb5wu3", pathname:"/a/123" }}/>
Redirect组件是V5中的页面重定向,属性和Link相同,用法如下:
// 基本的重定向
<Redirect to="/somewhere/else"/>
// 对象形式
// 采用push生成新的记录
<Redirect push to="/somewhere/else"/>
// 配合Switch组件使用,form表示重定向之前的路径,如果匹配则重定向,不匹配则不重定向
<Switch>
<Redirect from='/old-path' to='/new-path' />
<Route path='/new-path' component={Place} />
Switch>
注:
①页面重定向:客户端向服务器端发送了两次请求
②请求转发:客户端向服务器发送了一次请求
Navigate是V6中的页面重定向,使用方式如下:
<Route path="/" element={ to="/home" /> }>Route>
Switch组件是V5中进行路由切换的方式,类似Tab标签。Switch内部只能包含Route、Redirect、Router,示例代码如下:
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/:user" component={User} />
<Route component={NoMatch} />
Switch>
Routes组件是V6中进行路由切换的方式,示例代码如下:
<Routes>
<Route path="/" element={ /> } />
<Route path="/about" element={ /> } />
Routes>
withRouter组件:包装器,将普通的组件包装成路由组件。包装后普通组件就可以访问路由信息(如:history、location、match)
import { withRouter } from 'react-router-dom'
const MyComponent = (props) => {
const { match, location, history } = this.props
return (
<div>{props.location.pathname}</div>
)
}
const FirstTest = withRouter(MyComponent);
在Router5.x中新增加了Router Hooks用于在函数组件中获取路由信息。使用规则和React的其他Hooks一致。
(1)useHistory:返回history对象
(2)useLocation:返回location对象
(3)useRouteMatch:返回match对象
(4)useParams:返回match对象中的params,也就是path传递的参数
import React from "react";
import {useHistory} from "react - router - dom";
function backBtn(props) {
let history = useHistory;
return <button onClick={() => {
history.goBack();
}}>返回上一页</button>
}
在每个路由组件中我们可以使用this.props.history获取到history对象,也可以使用withRouter包裹组件获取,在history中封装了push,replace,go等方法,具体内容如下:
History {
length: number;
action: Action;
location: Location;
push(path: Path, state?: LocationState): void; // 调用push前进到一个地址,可以接受一个state对象,就是自定义的路由数据
push(location: LocationDescriptorObject): void; // 接受一个location的描述对象
replace(path: Path, state?: LocationState): void; // 用页面替换当前的路径,不可再goBack
replace(location: LocationDescriptorObject): void; // 同上
go(n: number): void; // 往前走多少也页面
goBack(): void; // 返回一个页面
goForward(): void; // 前进一个页面
block(prompt?: boolean | string | TransitionPromptHook): UnregisterCallback;
listen(listener: LocationListener): UnregisterCallback;
createHref(location: LocationDescriptorObject): Href;
}
404视图:404错误表示客户端请求的资源不存在。在react中请求的路径不存在(404),路由采用Switch组件进行切换
<Switch>
<Route exact={true} path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
<Route component={View404}/>
</Switch>
启动程序,默认是Home界面,这时的地址是:localhost:3000

点击About界面,跳转到About界面,这时地址是:localhost:3000/about

点击Topics界面,跳转到Topics界面,这时地址是:localhost:3000/topics

点击话题1,页面跳转,这时地址是:localhost:3000/topics/话题1

点击话题2,地址是:localhost:3000/topics/话题2

点击话题3,地址是:localhost:3000/topics/话题3

1、用WebStorm创建一个React项目react-demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):
npm install [email protected]
2、在src文件夹下新建文件components,用来编写不同的组件。在components文件夹下新建文件home.js,编写主页文件:
import React from "react";
class Home extends React.Component {
render() {
return (
<div>
<h2>Home页面</h2>
</div>
);
}
}
export default Home;
3、在components文件夹下新建about.js文件,代码如下:
import React from "react";
class About extends React.Component {
render() {
return (
<div>
<h2>About页面</h2>
</div>
)
}
}
export default About;
4、在components文件夹下新建topic.js文件,具体代码如下:
import React from "react";
class Topic extends React.Component {
render() {
return (
<div>
<h2>
{/*使用大括号表示这里的参数不止一条,需要从别的地方获取*/}
{/*this.props.match包含了url的信息,其中params包含了我们给定的参数*/}
{this.props.match.params.topicId}
</h2>
</div>
)
}
}
export default Topic;
5、在components文件夹下新建文件topics.js,代码如下:
import React from "react";
import {Link, Route} from "react-router-dom";
import Topic from "./topic";
class Topics extends React.Component {
render() {
return (
<div>
<h2>今日话题</h2>
<ul>
<li>
{/*Link表示要跳转到哪个页面,但并没有实现跳转,this.props.match.url获取到了当前页面的url*/}
<Link to={`${this.props.match.url}/话题1`}>
话题1:今天吃了什么
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/话题2`}>
话题2:今天做核酸了吗
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/话题3`}>
话题3:今天天气怎么样
</Link>
</li>
</ul>
{/*真正实现页面功能的是Route组件,exact表示默认显示当前信息*/}
{/*这里写render函数表示在页面加载出来之后首先显示该信息*/}
<Route exact path={this.props.match.url} render={() => (<h3>请选择今日话题</h3>)}/>
{/*当点击上面的话题时,跳转到topic界面,:topicId表示请求参数*/}
<Route path={`${this.props.match.url}/:topicId`} component={Topic}/>
</div>
);
}
}
export default Topics;
6、最后一步:修改App.js部分代码,将组件显示在网页上,App.js代码如下:
import {BrowserRouter, Link, Route} from "react-router-dom";
import Home from "./components/home";
import About from "./components/about";
import Topics from "./components/topics";
function App() {
return (
<BrowserRouter>
{/*编写Home、About、Topics页面的跳转*/}
<div>
<ul>
<li>
<Link to={"/"}>Home界面</Link>
</li>
<li>
<Link to={"/about"}>About界面</Link>
</li>
<li>
<Link to={"/topics"}>Topics界面</Link>
</li>
</ul>
{/*使用Route实现真正的页面跳转*/}
{/*当前默认主页面是Home界面*/}
<Route exact path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
</div>
</BrowserRouter>
)
}
export default App;
首页,localhost:3000:

新闻,localhost:3000/news:

点击新闻2,localhost:3000/news/1002:

课程,localhost:3000/course:

点击大数据,localhost:3000/big-data

加入我们,该页面没有编写,因此只能看到地址的改变,localhost:3000/joinUs:
![]()
1、用WebStorm创建一个React项目demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):
npm install [email protected]
2、在src文件夹下新建文件夹components和css,components用来存放编写的组件,css用来存放样式,在components文件夹下新建文件Headers.js,编写网页的头部,代码如下:
import React, {Component} from "react";
import {NavLink} from "react-router-dom";
import "../css/header.css";
class Headers extends Component {
render() {
<header>
<nav>
<ul>
<li>
<NavLink exact to={"/"}>首页</NavLink>
</li>
<li>
<NavLink to={"/news"}>新闻</NavLink>
</li>
<li>
<NavLink to={"/course"}>课程</NavLink>
</li>
<li>
<NavLink to={"/joinUs"}>加入我们</NavLink>
</li>
</ul>
</nav>
</header>
}
}
export default Headers;
![]()
3、在css文件夹下新建header.css文件,编写css样式,代码如下:
body {
font-size: 16px;
margin: 0;
padding: 0;
}
ul {
text-align: right;
background-color: #eee;
margin: 0;
}
ul li {
display: inline-block;
list-style: none;
text-align: center;
border-left: 1px solid #ccc;
}
a {
text-decoration: none;
color: #666;
font-size: 1.5rem;
padding: 0.8em 2em;
display: block;
}
a:hover {
color: #000;
}
a:active {
background-color: #666;
color: #fff;
}
4、在src文件夹下新建文件夹pages,主要存放页面组件,在该文件夹下新建Home.js文件,用来显示主页。主页是一个计数器,代码如下:
import React, {Component} from "react";
import Headers from "../components/Headers";
import "../css/home.css";
class Home extends Component {
constructor(props) {
super(props);
// 设置状态机
this.state = {
count: 0
}
}
// 编写增加函数
add = () => {
// preState表示当前状态state
this.setState((preState) => {
return {
count: preState.count + 1
}
})
}
// 编写减函数
sub = () => {
this.setState((preState) => {
return {
count: preState.count - 1
}
})
}
// 异步函数
async asyncAdd() {
// 表示1秒后再加1
await setTimeout(() => {
this.setState((preState) => {
return {
count: preState.count + 1
}
})
}, 1000);
}
render() {
return (
<div className={"home"}>
<Headers/>
<h1>Count的值:{this.state.count}</h1>
<div className={"flexContainer"}>
<button onClick={() => this.asyncAdd()}>等待1s再执行count+1</button>
<button onClick={this.add}>count+1</button>
<button onClick={() => this.sub()}>count-1</button>
</div>
</div>
)
}
}
export default Home;
5、在css文件夹下新建文件home.css,代码如下:
@keyframes rotate {
0% {
transform: rotate(0deg);
left: 0px;
}
100% {
transform: rotate(360deg);
left: 0px;
}
}
.home {
text-align: center;
}
.logo {
animation: rotate 10s linear 0s infinite;
}
button {
background: #237889;
font-size: calc(1.5 * 1rem);
color: #fff;
padding: 0.3rem 1rem;
border-radius: 1em;
margin: 1em;
}
6、在pages文件夹下新建文件NewDetails.js,编写新闻详情页,代码如下:
import React, {Component} from "react";
import Headers from "../components/Headers";
class NewDetails extends Component {
constructor(props) {
super(props);
// props.location.state存放了新闻的具体的数据id、content以及title
this.data = props.location.state ? props.location.state.data : null;
}
render() {
if (this.data != null) {
let title = this.data.title;
let content = this.data.content;
return (
<div>
<Headers/>
<h1>{title}</h1>
<p>{content}</p>
</div>
)
}
}
}
export default NewDetails;
7、在pages文件夹下新建News.js,编写新闻主页,代码如下:
import React, {Component} from "react";
import {Route, NavLink} from "react-router-dom";
import Headers from "../components/Headers";
import NewDetails from "./NewDetails";
const data = [ // 定义新闻的内容,在实际开发中这些数据来源于后台
{
id: 1001,
title: "新闻1",
content: "北京"
}, {
id: 1002,
title: "新闻2",
content: "上海"
}
]
class NewsPage extends Component {
render() {
return (
<div>
<Headers/>
<h1>请选择一条新闻</h1>
{
data.map((item) => {
return (
<div key={item.id}>
<NavLink to={{
pathname: `${this.props.match.url}/${item.id}`,
state: {data: item}
}}>
{item.title}
</NavLink>
</div>
)
})
}
</div>
)
}
}
// Route组件实现真正的跳转
const News = ({match}) => {
return (
<div>
<Route exact path={match.path} render={(props) => <NewsPage {...props}/>}/>
<Route path={`${match.path}/:id`} component={NewDetails}/>
</div>
)
}
export default News;
8、在pages文件夹下新建文件Course.js,代码如下:
import React, {Component} from "react";
import Headers from "../components/Headers";
import {NavLink} from "react-router-dom";
class Course extends Component {
render() {
let {match} = this.props; // this.props.match存放了path和url
return (
<div>
<Headers/>
{/*浏览器的路径改变了,但是没有发生页面跳转*/}
<p>
<NavLink to={`${match.url}/front-end`}>前端技术</NavLink>
</p>
<p>
<NavLink to={`${match.url}/big-data`}>大数据</NavLink>
</p>
<p>
<NavLink to={`${match.url}/algorithm`}>算法</NavLink>
</p>
</div>
);
}
}
export default Course;
9、最后一步,在App.js文件中修改部分代码,具体代码如下:
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
import Home from "./pages/Home";
import Course from "./pages/Course";
import News from "./pages/News";
function App() {
return (
<Router>
<Switch>
<Route exact path={"/"} component={Home}/>
<Route path={"/course"} component={Course}/>
<Route path={"/news"} component={News}/>
</Switch>
</Router>
);
}
export default App;