一. 单页面富应用(SPA)
单页面富应用的理解:
- 单页面富应用的英文是single-page application,简称SPA;
- 整个Web应用只有实际上只有一个页面,当URL发生改变时,并不会从服务器请求新的静态资源;
- 而是通过JavaScript监听URL的改变,并且根据URL的不同去渲染新的页面;
如何可以应用URL和渲染的页面呢?前端路由
- 前端路由维护着URL和渲染页面的映射关系;
- 路由可以根据不同的URL,最终让我们的框架(比如Vue、React、Angular)去渲染不同的组件;
- 最终我们在页面上看到的实际就是渲染的一个个组件页面;
二. 前端路由的原理
前端路由是如何做到URL和内容进行映射呢?
- 监听URL的改变。
- 原理
- 改变URL的时候,同时不引起页面的刷新
- 当监听到URL发生变化时,我们可以通过自己判断当前的URL,决定到底渲染什么样的内容。
URL发生变化,同时不引起页面的刷新有两个办法:
- 通过URL的hash改变URL;
- 通过HTML5中的history模式修改URL;
2. URL的hash
- URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
- 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
- 会直接在原来url的后面加a元素的href
//html部分
<div id="app">
<a href="#/home">首页</a>
<a href="#/about">关于</a>
<div class="page"></div>
</div>
//script部分
<script>
const page=document.querySelector('.page')
window.addEventListener('hashchange',()=>{ //监听hash的变化
console.log('有变化',location.hash); //location.hash获取当前的hash
switch (location.hash) {
case '#/home':
page.innerHTML='关于'
break;
case '#/about':
page.innerHTML='首页'
break;
default:
page.innerHTML=''
break;
}
})
</script>
注意:
- hash的优势就是兼容性更好,在老版IE中都可以运行;
- 但是缺陷是有一个#,显得不像一个真实的路径;
3. HTML5的History
history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:
- replaceState:替换原来的路径;
- pushState:使用新的路径;
- popState:路径的回退;
- go:向前或向后改变路径;
- forword:向前改变路径;
- back:向后改变路径;
<div id="app">
<a href="home">首页</a> //a元素的默认行为是会跳转到一个新的页面
<a href="/about">关于</a>
<div class="page"></div>
<script>
const page=document.querySelector('.page')
const as=document.getElementsByTagName('a')
for(let a of as){
a.addEventListener('click',(e)=>{
e.preventDefault(); //阻止a元素的默认行为
const href=a.getAttribute('href') //获取a元素的自由属性
// console.log('a元素被点击');
// history.pushState({},'','abc')//第三个参数是要跳转的新的路径
history.pushState({},'',href)
//第三个属性如果传入的是'abc',他会在原来的路径下跳转
//如果传入的是'/abc',他会跳转到一个全新的路径,不在原来的路径下
//路径指的是端口后面的东西
urlChange()
})
}
//执行返回操作时,依然来到urlchange
window.addEventListener('popstate',urlChange)
//监听url的变化
function urlChange(){
console.log(location.pathname);//location.pathname是取当前路径,
//相当于上面的href
switch (location.pathname) { //路径指的是端口后面的东西
case '/home':
page.innerHTML='关于'
break;
case '/about':
page.innerHTML='首页'
break;
default:
page.innerHTML=''
break;
}
}
</script>
</div>
//18行
三. react-router
React项目的可用的路由库是React-Router,当然这也是官方支持的。它也分为:
react-router 核心组件
react-router-dom 应用于浏览器端的路由库(单独使用包含了react-router的核心部分)
react-router-native 应用于native端的路由
1. Router基本使用
安装react-router:
- 安装react-router-dom会自动帮助我们安装react-router的依赖;
yarn add react-router-dom
react-router最主要的API是给我们提供的一些组件:
(1)BrowserRouter和HashRouter
- 包含了对路径改变的监听,并且会将相应的路径传递给子组件;
- BrowserRouter或HashRouter包裹组件
- BrowserRouter使用history模式;
- HashRouter使用hash模式;
(2)Link和NavLink:
- 通常路径的跳转是使用Link组件,最终会被渲染成a元素;
- NavLink是在Link基础之上增加了一些样式属性;
- to属性:Link中最重要的属性,用于设置跳转到的路径;
(3)Route:
- Route用于路径的匹配;
- 默认情况下匹配的是模糊匹配,只要前面的匹配到了就会跳转到该页
- path属性:用于设置匹配到的路径;
- component属性:设置匹配到路径后,渲染的组件;
- exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
- Route放在link前面会在link上面渲染这个组件,看房的位置,不是固定的
//先导入
import {
HashRouter,
Link,
Route
} from 'react-router-dom'
export default class App extends PureComponent {
render() {
return (
<div>
<HashRouter>
<Link to='/' >首页</Link>
<Link to='/about' >关于</Link>
<Route exact path='/' component={Home}/>
<Route path='/about' component={About}/>
</HashRouter>
</div>
)
}
}
2. NavLink的使用
需求:路径选中时,对应的a元素变为红色
这个时候,我们要使用NavLink组件来替代Link组件,NavLink自由的属性,其他跟link没有区别
- activeStyle:活跃时(匹配时)的样式;
- activeClassName:活跃时添加的class;
- exact:是否精准匹配;
<NavLink exact to='/' activeStyle={{color:'red'}}>首页</NavLink>
<NavLink to='/about' activeStyle={{color:'red'}}>关于</NavLink>
但是,我们会发现在选中about或profile时,第一个也会变成红色:
- 原因是/路径也匹配到了/about或/profile;
- 这个时候,我们可以在第一个NavLink中添加上exact属性;
默认的activeClassName:
- 事实上在默认匹配成功时,NavLink就会添加上一个动态的active class;
- 所以我们也可以直接编写样式
a.active {
color: red;
}
当然,如果你担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class
<NavLink exact to="/" activeClassName="link-active">首页</NavLink>
<NavLink to="/about" activeClassName="link-active">关于</NavLink>
<NavLink to="/profile" activeClassName="link-active">我的</NavLink>
3. Switch的作用
默认的路由规则:模糊匹配
- 当我们匹配到某一个路径时,我们会发现有一些问题;
- 比如/about路径匹配到的同时,/:userid也被匹配到了,并且最后的一个NoMatch组件总是被匹配到;
原因是默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染;
但是实际开发中,我们往往希望有一种排他的思想:
- 只要匹配到了第一个,那么后面的就不应该继续匹配了;
- 这个时候我们可以使用Switch来将所有的Route进行包裹即可;
4. Redirect
Redirect用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
就是我们默认打开一个网站,他不经过我们的点击直接跳转到另外一个网站
之前的link或者是navlink是不会跳转的,它必须经过我们的一个交互
我们这里使用这个的一个案例:
- 用户跳转到User界面;
- 但是在User界面有一个isLogin用于记录用户是否登录:
- true:那么显示用户的名称;
- false:直接重定向到登录界面;
App.js中提前定义好Login页面对应的Route:
<Switch>
...其他Route
<Route path="/login" component={Login} />
<Route component={NoMatch} />
</Switch>
在User.js中写上对应的逻辑代码:
import React, { PureComponent } from 'react'
import { Redirect } from 'react-router-dom';
export default class User extends PureComponent {
constructor(props) {
super(props);
this.state = {
isLogin: false
}
}
render() {
return this.state.isLogin ? (
<div>
<h2>User</h2>
<h2>用户名: coderwhy</h2>
</div>
): <Redirect to="/login"/>
}
}
四. react-router高级使用
1. 路由嵌套
在开发中,路由之间是存在嵌套关系的。
这里我们假设about页面中有多个页面内容:
import React, { PureComponent } from 'react'
import {
HashRouter,
NavLink,
Route,
Switch
} from 'react-router-dom'
import About1 from './About1'
import About2 from './About2'
import About3 from './About3'
export default class about extends PureComponent {
render() {
return (
<div>
<NavLink exact to='/about' activeStyle={{color:'green'}}>推荐</NavLink>
<NavLink to='/about/about2' activeStyle={{color:'green'}}>歌手</NavLink>
<NavLink to='/about/about3' activeStyle={{color:'green'}}>艺术家</NavLink>
<Switch>
<Route exact path='/about' component={About1}/>
<Route path='/about/about2' component={About2}/>
<Route path='/about/about3' component={About3}/>
</Switch>
</div>
)
}
}
- 不管是Route还是NavLink建议第一个都加上exact
- 注意Switch的特点,在about的子路由下,如果第一个不加exact,则页面匹配到的永远都是path=’/about’,页面不会变化
2. 手动跳转
目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码进行跳转。
但是通过JavaScript代码进行跳转有一个前提:必须获取到history对象。
如何可以获取到history的对象呢?两种方式
- 方式一:如果该组件是通过路由直接跳转过来的,那么可以直接获取history、location、match对象;
这种方式只能是一级导航用了BrowserRouter或HashRouter的条件下他的子导航中使用
因为普通组件没有history、location、match对象,是router提供了这些对象
export default class about extends PureComponent {
render() {
return (
<div>
<NavLink exact to='/about' activeStyle={{color:'green'}}>推荐</NavLink>
<NavLink to='/about/about2' activeStyle={{color:'green'}}>歌手</NavLink>
<NavLink to='/about/about3' activeStyle={{color:'green'}}>艺术家</NavLink>
<button onClick={e=>{this.shop()}}>商品列表</button>
<Switch>
<Route exact path='/about' component={About1}/>
<Route path='/about/about2' component={About2}/>
<Route path='/about/about3' component={About3}/>
<Route path='/about/about3' component={About3}/>
<Route path='/about/about4' component={About3}/>
</Switch>
</div>
)
}
shop(){
this.props.history.push('/about/about4');
}
}
- 方式二:如果该组件是一个普通渲染的组件,那么不可以直接获取history、location、match对象;
如果是在一级导航中用上面的方法没有任何效果
那么如果普通的组件也希望获取对应的对象属性应该怎么做呢?
- 前面我们学习过高阶组件,可以在组件中添加想要的属性;
- react-router也是通过高阶组件为我们的组件添加相关的属性的;
如果我们希望在App组件中获取到history对象,必须满足一下两个条件:
- App组件必须包裹在Router组件之内;
- App组件使用withRouter高阶组件包裹;
index.js代码修改如下:
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
App.js代码修改如下:
import { Route, Switch, NavLink, withRouter } from 'react-router-dom';
...省略其他的导入代码
class App extends PureComponent {
render() {
console.log(this.props.history);
return (
<div>
...其他代码
<button onClick={e => this.pushToProfile()}>我的</button>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
<Route path="/:userid" component={User} />
<Route component={NoMatch} />
</Switch>
</div>
)
}
pushToProfile() {
this.props.history.push("/profile");
}
}
export default withRouter(App);
3. 传递参数
传递参数有三种方式:
- 动态路由的方式;
- search传递参数;
- to传入对象;
(1)动态路由的方式
动态路由的概念指的是路由中的路径并不会固定:
如果直接写to=’/about’,他这个路由是固定的
- 比如/detail的path对应一个组件Detail;
- 如果我们将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示;多对一的关系
- 这个匹配规则,我们就称之为动态路由;
通常情况下,使用动态路由可以为路由传递参数。
//要跳转的id
const id='abc';
//最后是要跳转到/about6/abc
<NavLink to={`/about6/:${id}`} activeStyle={{color:'red'}}>关于</NavLink>
//定义Route
<Route path='/about6/:id' component={About6}/> //固定写法
<div>
...其他Link
<NavLink to="/detail/abc123">详情</NavLink>
<Switch>
... 其他Route
<Route path="/detail/:id" component={Detail}/>
<Route component={NoMatch} />
</Switch>
</div>
About6.js的代码如下:
我们可以直接通过match对象中获取id;
这里我们没有使用withRouter,原因是因为About6本身就是通过路由进行的跳转;
之所以在About6页面获取id是因为可以根据id的不同展示不同的内容
import React, { PureComponent } from 'react'
export default class About6 extends PureComponent {
render() {
const match=this.props.match //获取当前路由的match
console.log(match);
console.log(match.params.id); //id保存在match.params对象中
return (
<div>
<h2>About6动态路由:{match.params.id}</h2>
</div>
)
}
}
(2)search传递参数
NavLink写法:
- 我们在跳转的路径中添加了一些query参数;
<NavLink to={`/about7?name=coderwhy&age=18`} activeStyle={{color:'red'}}>详情2</NavLink>
<Route path='/about7' component={About7}/>
About7中如何获取呢?
- About7中是需要在location中获取search的;
import React, { PureComponent } from 'react'
export default class About7 extends PureComponent {
render() {
console.log(this.props.location);
return (
<div>to传递参数:{this.props.location.search}</div>
)
}
}
- 注意:这个search没有被解析,需要我们自己来解析;所以不推荐用这种方式传递参数
(3)to传入对象
to可以直接传入一个对象
<NavLink to={{
pathname: "/detail2", //必须要写这个属性,是要跳转的页面
query: {name: "kobe", age: 30},
state: {height: 1.98, address: "洛杉矶"},
search: "?apikey=123"
}} activeStyle={{color:'red'}}>详情3</NavLink>
<Route path='/about8' component={About8}/>
About8页面中获取这个对象
import React, { PureComponent } from 'react'
export default class About8 extends PureComponent {
render() {
console.log(this.props.location);
return (
<div>
About8页面
to传递参数
</div>
)
}
}
五. react-router-config
目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。
但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
- 这个时候可以使用react-router-config来完成;
安装react-router-config:yarn add react-router-config
在src目录下新建一个route
常见router/index.js文件:
import Home from "../pages/home";
import About, { AboutMessage, AboutProduct } from "../pages/about";
import Profile from "../pages/profile";
import Login from "../pages/login";
import User from "../pages/user";
import Detail from "../pages/detail";
import Detail2 from "../pages/detail2";
import NoMatch from "../pages/nomatch";
const routes = [
{
path: "/", //要跳转的页面
exact: true, //建议不管是导航还是子导航,第一个都加上这个属性,模糊匹配问题
component: Home //跳转过去之后要展示的组件
},
{
path: "/about",
component: About,
routes: [ //相当于about下面有子导航
{
path: "/about",
exact: true,
component: AboutProduct
},
{
path: "/about/message",
component: AboutMessage
},
]
},
{
path: "/profile",
component: Profile
},
{
path: "/login",
component: Login
},
{
path: "/user",
component: User
},
{
path: "/detail/:id",
component: Detail
},
{
path: "/detail2",
component: Detail2
},
{
component: NoMatch
}
];
export default routes;
将之前的Switch配置,换成react-router-config中提供的函数:
import {renderRoutes} from 'react-route-config'
import routes from './router/index'
//把Switch直接换成
{renderRoutes(routes)}
{/* <Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
<Route path="/user" component={User} />
<Route path="/login" component={Login} />
<Route path="/detail/:id" component={Detail}/>
<Route path="/detail2" component={Detail2}/>
<Route component={NoMatch} />
</Switch> */}
当点击某一个导航,有颜色变化,但是内容没有变化时,一定要考虑exact
如果说About导航下面有子导航,跟上面同理
//1.先在about.js页面中导入
import {renderRoutes} from 'react-route-config'
//把原来的Switch换成
//但是一定要注意子导航不是直接传入routes
//routes是总的路由
//给他传的应该是about大导航下面的routes
//在这个页面中获取整个大导航this.props.routes
//获取这个子导航this.props.routes.routes
renderRoutes(this.props.routes.routes) //要注意第一个路径中添加exact
实际上react-router-config中还提供了一个matchRoutes辅助函数:
matchRoutes(routes, pathname)传入一个路由对象数组,获取所有匹配的路径;
const routes = matchRoutes(this.props.route.routes, "/about");
console.log(routes);