React页面路由
React页面路由
前言:
随着 ajax 的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是单页Web应用(Single Page Web Application,SPA))的兴起,前端路由系统随之开始流行。
1、从用户的角度看,前端路由主要实现了两个功能(使用ajax更新页面状态的情况下):
记录当前页面的状态(保存或分享当前页的url,再次打开该url时,网页还是保存(分享)时的状态);
可以使用浏览器的前进后退功能(如点击后退按钮,可以使页面回到使用ajax更新页面之前的状态,url也回到之前的状态);
2、作为开发者,要实现这两个功能,我们需要做到:
改变url且不让浏览器向服务器发出请求;
监测 url 的变化;
截获 url 地址,并解析出需要的信息来匹配路由规则。
我们路由常用的hash模式和history模式实际上就是实现了上面的功能。
一、hash模式
这里的 hash 就是指 url 尾巴后的 # 号以及后面的字符。这里的 # 和 css 里的 # 是一个意思。hash也称作锚点,本身是用来做页面定位的,她可以使对应 id 的元素显示在可视区域内。
由于 hash 值变化不会导致浏览器向服务器发出请求,而且 hash 改变会触发 hashchange 事件,浏览器的进后退也能对其进行控制,所以人们在 html5 的 history 出现前,基本都是使用 hash 来实现前端路由的。
使用到的api:
window.location.hash = 'qq' // 设置 url 的 hash,会在当前url后加上 '#qq'
var hash = window.location.hash // '#qq'
window.addEventListener('hashchange', function(){
// 监听hash变化,点击浏览器的前进后退会触发
})
二、history模式
已经有 hash 模式了,而且 hash 能兼容到IE8, history 只能兼容到 IE10,为什么还要搞个 history 呢?
首先,hash 本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。
其次,hash 的传参是基于 url的,如果要传递复杂的数据,会有体积的限制,而history 模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中。
相关API:
window.history.pushState(state, title, url)
// state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
// title:标题,基本没用,一般传 null
// url:设定新的历史记录的 url。新的 url 与当前 url 的 origin 必须是一樣的,否则会抛出错误。url可以是绝对路径,也可以是相对路径。
例如:
当前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/
window.history.replaceState(state, title, url)
// 与 pushState 基本相同,但她是修改当前历史记录,而 pushState 是创建新的历史记录
window.addEventListener("popstate", function() {
// 监听浏览器前进后退事件,pushState 与 replaceState 方法不会触发
});
window.history.back() // 后退
window.history.forward() // 前进
window.history.go(1) // 前进一步,-2为后退两步,window.history.length可以查看当前历史堆栈中页面的数量
三、react-router-dom API
React实现页面路由的模块:react-router-dom
1、HashRouter和BrowserRouter
其实就是路由的hash和history两种模式,并且这两个组件是路由的容器,必须在最外层
// hash模式
ReactDOM.render(
<HashRouter>
<Route path="/" component={Home}/>
</HashRouter>
)
// history模式
ReactDOM.render(
<BrowserRouter>
<Route path="/" component={Home} />
</BrowserRouter>
)
2、Route
路由的一个原材料,它是控制路径对应显示的组件
Route的参数
path:跳转的路径
component: 对应路径显示的组件
render:可以自己写render函数返回具体的dom,而不需要去设置component
location: 传递route对象,和当前的route对象对比,如果匹配则跳转
exact: 匹配规则,true的时候则精确匹配。
3、Router
低级路由,适用于任何路由组件,主要和redux深度集成,使用必须配合history对象,使用Router路由的目的是和状态管理库如redux中的history同步对接
<Router history={history}>
...
</Router>
4、Link和NavLink
两者都是跳转路由,NavLink的参数更多些
(1)Link的api
l to: 有两种写法,表示跳转到哪个路由
- 字符串写法
<Link to="/a" />
- 对象写法
<Link to={{
pathname: '/courses',
search: '?sort=name',
hash: '#the-hash',
state: { fromDashboard: true }
}}/>
l replace:就是将push改成replace
l innerRef:访问Link标签的dom
(2)NavLink的api
l Link的所有api
l activeClassName 路由激活的时候设置的类名
l activeStyle 路由激活设置的样式
l exact 参考Route,符合这个条件才会激活active类
l strict 参考Route,符合这个条件才会激活active类
l 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>
l location 接收一个location对象,当url满足这个对象的条件才会跳转
<NavLink to="/a/123" location={{ key:"mb5wu3", pathname:"/a/123" }}/>
5、Redirect:页面重定向
// 基本的重定向
<Redirect to="/somewhere/else" />
// 对象形式
<Redirect
to={{
pathname: "/login",
search: "?utm=your+face",
state: { referrer: currentLocation }
}}
/>
// 采用push生成新的记录
<Redirect push to="/somewhere/else" />
// 配合Switch组件使用,form表示重定向之前的路径,如果匹配则重定向,不匹配则不重定向
<Switch>
<Redirect from='/old-path' to='/new-path'/>
<Route path='/new-path' component={Place}/>
</Switch>
6、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>
7、withRouter
当一个非路由组件也想访问到当前路由的match,location,history对象,那么withRouter将是一个非常好的选择,可以理解为将一个组件包裹成路由组件
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);
8、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>
}
9、history对象
在每个路由组件中我们可以使用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;
}
这样我们想使用api来操作前进后退就可以调用history中的方法
10、404视图
既然路由就需要考虑一个问题---404视图。当用户访问一些不存在的URL时就该返回404视图了,但不存在的地址该如何匹配呢?----使用Switch
Switch组件的作用类似于JS中的switch语句,当一项匹配成功之后,就不再匹配后续内容。这样的话就可以把要匹配的内容写在Switch组件中,最后一项写404视图,当其他都匹配不成功时就是404视图。例如:
<Switch>
<Route exact={true} path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
<Route component={View404}/>
</Switch>
四、react-router-dom实现路由
1、基本路由示例:
(1)Home.js
import React, { Component } from "react";
class Home extends Component {
render() {
return (
<div>
<h2>Home页面</h2>
</div>
)
}
}
export default Home;
(2)About.js
import React, { Component } from "react";
class About extends Component {
render() {
return (
<div>
<h2>About页面</h2>
</div>
)
}
}
export default About;
(3)Topic.js
import React,{ Component } from "react";
class Topic extends Component {
render() {
console.log(this.props);
return (
<div>
<h2>{ this.props.match.params.topicId}</h2>
</div>
)
}
}
export default Topic;
(4)Topics.js
import React,{ Component } from "react";
import {
Route,
Link
} from "react-router-dom";
import Topic from "./Topic";
class Topics extends Component{
render() {
console.log(this)
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${ this.props.match.url }/rendering`}>
Rendering with React
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/components`}>
Components
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/props-v-state`}>
Props v. State
</Link>
</li>
</ul>
<Route path={`${this.props.match.url}/:topicId`} component={Topic}/>
<Route exact path={this.props.match.url} render={()=> (
<h3> Please select a topic.</h3>
)}/>
</div>
)
}
}
export default Topics;
(5)App.js
import logo from './logo.svg';
import './App.css';
import {
BrowserRouter as Router,
Route,
Link
} from "react-router-dom";
import Topics from "./components/Topics";
import Home from "./components/Home";
import About from "./components/About";
function App() {
return (
<Router>
<div>
<ul>
<li>
<Link to={"/"}>Home</Link>
</li>
<li>
<Link to={"/about"}>About</Link>
</li>
<li>
<Link to={"/topics"}>Topics</Link>
</li>
</ul>
<hr/>
<Route exact={true} path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
</div>
</Router>
)
}
export default App;
2、嵌套路由示例
(1)运行结果
(2)目录结构
(3)Header.js
import React,{ Component } from "react";
import { NavLink } from "react-router-dom";
import '../css/header.css';
class Header extends Component {
render() {
return (
<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 Header;
(4)header.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;
}
(5)Home.js
import React,{ Component } from "react";
import Header from "../components/Header";
import '../css/home.css';
import logo from '../images/react.png';
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"}>
<Header/>
<div>
<img className="logo" src={logo} alt={"logo"}/>
</div>
<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;
(6)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.3em 1em;
border-radius: 1em;
margin: 1em;
}
(7)NewDetail.js
import React,{ Component } from "react";
import Header from "../components/Header";
class NewDetail extends Component{
constructor(props) {
super(props);
console.log(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>
<Header />
<h1>{ title }</h1>
<p> { content }</p>
</div>
)
}
}
}
export default NewDetail;
(8)News.js
import React,{ Component } from "react";
import {
Route,
NavLink
} from "react-router-dom";
import Header from "../components/Header";
import NewDetail from "./NewDetail";
const data = [
{
id: 1001,
title: '西安新增6例新冠病例',
content: '上海-西安-张掖-额济纳-西安'
},
{
id: 1002,
title: '寒潮来袭,你...冻成狗了吗?',
content: '被子是我的亲人,我不想离开它'
}
]
class NewsPage extends Component{
render() {
return(
<div>
<Header />
<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={NewDetail} />
</div>
)
}
export default News;
(9)Course.js
import React,{ Component } from "react";
import Header from "../components/Header";
import { NavLink } from "react-router-dom";
class Course extends Component{
render() {
let { match } = this.props;
console.log(this.props)
return(
<div>
<Header />
<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;
(10)App.js
import './App.css';
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;
五、关于路由的面试题:
1、实现前端路由的两种方式及其差异?
前端路由的本质是监听url变化,然后匹配路由规则,无需刷新就可以显示相应的页面,目前单页面路由主要有两种方式
(1) hash 模式
(2) history 模式
2、hash模式实现路由
为什么要使用hash模式:页面使用Ajax发送异步请求可以实现无缝刷新,但这种方式也存在使得浏览器的url不发生任何变化的时候就完成了请求,使得用户体验不佳,也导致了用户下次使用相同的url访问上次的页面时内容无法重新呈现的问题。hash模式是解决这个问题的途径之一。
主要通过location.hash设置hash Url,也就是url的符号#后面的值。当哈希值发生变化时,不会向服务器请求发送数据,可以通过hashchange事件来监听hash的变化,实现相应的功能
(1) ocation.hash 设置/获取hash
(2) hashchange事件监听url变化,解析url实现页面路由跳转
hash模式需要注意的几个点
- 散列值不会随请求发送到服务器
散列值会反映在浏览器url上
只修改浏览器的哈希部分,按下回车,浏览器不会发送任何请求给服务器,只会触发hashchange事件并且修改location.hash的值
- html a标签,设置id锚点,点击触发时,浏览器会自动设置location.hash值,同时触发hashchange事件,url上也会反映这一变化
- hash模式下,手动刷新页面不会向浏览器发送请求,不会触发hashchange事件,但是会触发load事件
示例1:location.hash触发hashchange事件
效果:
示例2:锚点跳转设置hash触发hashchange事件
关于锚点跳转:
- a标签可以跳转到指定了name或者id的a标签
- a标签可以跳转到指定了id的非a标签,非a标签如果没有指定id则不可以被跳转
3、history模式实现路由
主要通过history.pushState/replceState向当前历史记录中插入状态对象state,在浏览器前进、回退、跳转等动作发生时触发popState事件,此时可以通过解析popState事件回调函数的event参数中的state对象,或者解析当前页面url来实现路由。
建议解析url方式实现路由。如果没有在页面首次加载的时候设置pushState/replaceState,那么首页一般是没有state对象的,在执行浏览器动作时,如果回退到首页,那么当前history对象的state属性不存在,导致解析state报错
示例:
两种路由方式的差异以及需要注意的点
(1)方式的异同
页面手动刷新,hash模式不会向服务器发送请求,history模式会
- hash模式下url中的哈希值不会发送到服务器,history模式url全部会发送至服务器
设置location.hash和pushState都不会导致浏览器刷新
设置location.hash的时候会触发hashchange事件和popstate事件
仅当pushState函数设置url参数的值为hash格式时,浏览器动作发生会触发hashchange事件,尽管location.hash值为空
- a标签锚点跳转可以设置hash,触发hashchange事件
(2)注意的问题
如果pushState的url为跨域网址,那么会报错.这样设计的目的是防止恶意代码让用户以为他们是在另一个网站上
以上是 React页面路由 的全部内容, 来源链接: utcz.com/z/383561.html