1 背景
随着 ajax 的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是单页Web应用(Single Page Web Application,SPA)的兴起,前端路由系统随之开始流行。
- 从用户的角度看,前端路由主要实现了两个功能(使用ajax更新页面状态的情况下):
- 记录当前页面的状态(保存或分享当前页的url,再次打开该url时,网页还是保存(分享)时的状态);
- 可以使用浏览器的前进后退功能(如点击后退按钮,可以使页面回到使用ajax更新页面之前的状态,url也回到之前的状态);
- 作为开发者,要实现这两个功能,我们需要做到:
- 改变url且不让浏览器向服务器发出请求;
- 监测 url 的变化;
- 截获 url 地址,并解析出需要的信息来匹配路由规则。
我们路由常用的hash模式和history模式实际上就是实现了上面的功能。
2 hash模式
这里的 hash 就是指 url 尾巴后的 # 号以及后面的字符。这里的 # 和 css 里的 # 是一个意思。hash也称作锚点,本身是用来做页面定位的,它可以使对应 id 的元素显示在可视区域内。
由于 hash 值变化不会导致浏览器向服务器发出请求,而且 hash 改变会触发 hashchange 事件,浏览器的进后退也能对其进行控制,所以人们在 html5 的 history 出现前,基本都是使用 hash 来实现前端路由的。
缺点:传递参数的方式是在url后拼接,会有体积的限制
使用到的api:
window.location.hash = 'qq'
var hash = window.location.hash
window.addEventListener('hashchange', function () {
})
3 history模式
history模式的优点: (1)可以传递复杂的参数 (2)可以监听浏览器的前进、后退事件(back、forward、go)
history模式与hash模式的比较: 1、hash 本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。 2、hash 的传参是基于url的,如果要传递复杂的数据,会有体积的限制,而history模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。
相关API:
window.history.pushState(state, title, url)
window.history.replaceState(state, title, url)
window.addEventListener("popstate", function () {
});
window.history.back()
window.history.forward()
window.history.go(1)
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/
3 React实现页面路由的模块:react-router-dom
3.1 安装
npm install react-router-dom
3.2 HashRouter和BrowserRouter:路由的容器
HashRouter和BrowserRouter决定了路由模式分别是hash模式和history模式,并且这两个组件是路由的容器,必须放在最外层。
1、hash模式
ReactDOM.render(
<HashRouter>
<Route path="/" component={Home}/>
</HashRouter>
)
2、history模式
ReactDOM.render(
<BrowserRouter>
<Route path="/" component={Home}/>
</BrowserRouter>
)
3.3 Route:路由与组件之间的映射
Route组件:实现路径和显示组件之间的映射
<Route path="/users" component={组件} render={返回dom} location="route对象" exact="匹配规则"/>
它的参数如下:
参数 | 说明 |
---|
path | 跳转的路径 | component | 对应路径显示的组件 | render | 可以自己写render函数返回具体的dom,而不需要去设置component | location | 传递route对象,和当前的route对象对比,如果匹配则跳转 | exact | 匹配规则,默认值为false,true的时候则精确匹配。 |
不同版本的react-router-dom,Route的属性也不同,react-router-dom6.0以下的版本:
<Route path="/users" component={组件} render={返回dom} location="route对象" exact="匹配规则"/>
react-router-dom6.0(含6.0)以上的版本
<Route path="/users" element={组件} render={返回dom} location="route对象" exact="匹配规则"/>
示例:
<Routes>
<Route exact path={"/"} element={<home/>}/>
<Route path={"/about"} element={<About/>}/>
<Route path={"/topics"} element={<Topics/>}/>
</Routes>
3.4 Router:管理路由的状态
Router组件:底层路由,使用的前提是路由模式必须是history模式,可以管理路由的状态。
<Router history={history}>
</Router>
3.5 Link和NavLink:跳转链接
Link和NavLink组件:类似于<a> 标签
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) => {
console.log(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" }}/>
3.6 Redirect:页面重定向
Redirect组件:页面重定向,属性和Link相同
<Redirect to="/somewhere/else"/>
<Redirect
to={{
pathname: "/login",
search: "?utm=your+face",
state: {referrer: currentLocation}
}}
/>
<Redirect push to="/somewhere/else"/>
<Switch>
<Redirect from='/old-path' to='/new-path'/>
<Route path='/new-path' component={Place}/>
</Switch>
注: ①页面重定向:客户端向服务器端发送了两次请求 ②请求转发:客户端向服务器发送了一次请求
3.7 Switch:路由切换
Switch组件:进行路由切换,类似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>
3.8 withRouter:包装器
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);
3.9 Router Hooks:在函数组件中获取路由信息
在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>
}
3.10 history对象
在每个路由组件中我们可以使用this.props.history获取到history对象,也可以使用withRouter包裹组件获取,在history中封装了push,replace,go等方法,具体内容如下:
History {
length: number;
action: Action;
location: Location;
push(path: Path, state?: LocationState): void;
push(location: LocationDescriptorObject): void;
replace(path: Path, state?: LocationState): void;
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;
}
3.11 404视图:请求资源不存在
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>
4 react-router-dom实现路由案例
4.1 【案例1】实现简单的路由跳转
4.1.1 效果展示
启动程序,默认是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
4.1.2 实现步骤
1、用WebStorm创建一个React项目react-demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):
npm install react-router-dom@5.2.0
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.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 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 path={this.props.match.url} render={() => (<h3>请选择今日话题</h3>)}/>
{}
<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>
{}
<div>
<ul>
<li>
<Link to={"/"}>Home界面</Link>
</li>
<li>
<Link to={"/about"}>About界面</Link>
</li>
<li>
<Link to={"/topics"}>Topics界面</Link>
</li>
</ul>
{}
{}
<Route exact path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
</div>
</BrowserRouter>
)
}
export default App;
4.2 【案例2】实现计数器
4.2.1 效果展示
首页,localhost:3000: 新闻,localhost:3000/news: 点击新闻2,localhost:3000/news/1002: 课程,localhost:3000/course: 点击大数据,localhost:3000/big-data 加入我们,该页面没有编写,因此只能看到地址的改变,localhost:3000/joinUs:
4.2.2 实现步骤
1、用WebStorm创建一个React项目demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):
npm install react-router-dom@5.2.0
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 = () => {
this.setState((preState) => {
return {
count: preState.count + 1
}
})
}
sub = () => {
this.setState((preState) => {
return {
count: preState.count - 1
}
})
}
async asyncAdd() {
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);
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>
)
}
}
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;
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;
|