一、前端路由原理
前端路由是如何做到URL和内容进行映射呢?监听URL的改变。
URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
<div id="app">
<a href="#/home">home</a>
<a href="#/about">about</a>
<div class="router-view"></div>
</div>
<script>
// 1.获取router-view
const routerViewEl = document.querySelector(".router-view");
// 2.监听hashchange
window.addEventListener("hashchange", () => {
switch(location.hash) {
case "#/home":
routerViewEl.innerHTML = "home";
break;
case "#/about":
routerViewEl.innerHTML = "about";
break;
default:
routerViewEl.innerHTML = "default";
}
})
</script>
hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
HTML5的History
history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:
- replaceState:替换原来的路径;
- pushState:使用新的路径;
- popState:路径的回退;
- go:向前或向后改变路径;
- forword:向前改变路径;
- back:向后改变路径;
我们这里来简单演示几个方法:
<div id="app">
<a href="/home">home</a>
<a href="/about">about</a>
<div class="router-view"></div>
</div>
<script>
// 1.获取router-view
const routerViewEl = document.querySelector(".router-view");
// 2.监听所有的a元素
const aEls = document.getElementsByTagName("a");
for (let aEl of aEls) {
aEl.addEventListener("click", (e) => {
e.preventDefault();
const href = aEl.getAttribute("href");
console.log(href);
history.pushState({}, "", href);
historyChange();
})
}
// 3.监听popstate和go操作
window.addEventListener("popstate", historyChange);
window.addEventListener("go", historyChange);
// 4.执行设置页面操作
function historyChange() {
switch(location.pathname) {
case "/home":
routerViewEl.innerHTML = "home";
break;
case "/about":
routerViewEl.innerHTML = "about";
break;
default:
routerViewEl.innerHTML = "default";
}
}
</script>
二. react-router
React Router的版本4开始,路由不再集中在一个包中进行管理了:
react-router是router的核心部分代码;
react-router-dom是用于浏览器的;
react-router-native是用于原生应用的;
目前我们使用最新的React Router版本是v5的版本:
实际上v4的版本和v5的版本差异并不大;
安装react-router:
安装react-router-dom会自动帮助我们安装react-router的依赖;
yarn add react-router-dom
三. react-router基本使用
3.1. Router基本使用
react-router最主要的API是给我们提供的一些组件:
1. BrowserRouter或HashRouter 2. Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件; 3. BrowserRouter使用history模式; 4. HashRouter使用hash模式;Link和NavLink:
通常路径的跳转是使用Link组件,最终会被渲染成a元素;
NavLink是在Link基础之上增加了一些样式属性;
to属性:Link中最重要的属性,用于设置跳转到的路径;
Route:
Route用于路径的匹配;
path属性:用于设置匹配到的路径;
component属性:设置匹配到路径后,渲染的组件;
exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
eg:
import React, { PureComponent } from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom';
import Home from './pages/home';
import About from './pages/about';
import Profile from './pages/profile';
export default class App extends PureComponent {
render() {
return (
<BrowserRouter>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
</BrowserRouter>
)
}
}
3.2. NavLink的使用
路径选中时,对应的a元素变为红色
这个时候,我们要使用NavLink组件来替代Link组件:
activeStyle:活跃时(匹配时)的样式;
activeClassName:活跃时添加的class;
exact:是否精准匹配;
先演示activeStyle:
<NavLink to="/" activeStyle={{color: "red"}}>首页</NavLink>
<NavLink to="/about" activeStyle={{color: "red"}}>关于</NavLink>
<NavLink to="/profile" activeStyle={{color: "red"}}>我的</NavLink>
但是,我们会发现在选中about或profile时,第一个也会变成红色:
原因是/路径也匹配到了/about或/profile;
这个时候,我们可以在第一个NavLink中添加上exact属性;
<NavLink exact to="/" activeStyle={{color: "red"}}>首页</NavLink>
默认的activeClassName:
事实上在默认匹配成功时,NavLink就会添加上一个动态的active class;
当然,如果你担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class
<NavLink exact to="/" activeClassName="link-active">首页</NavLink>
<NavLink to="/about" activeClassName="link-active">关于</NavLink>
<NavLink to="/profile" activeClassName="link-active">我的</NavLink>
3.3. Switch的作用
我们来看下面的路由规则:
当我们匹配到某一个路径时,我们会发现有一些问题;
比如/about路径匹配到的同时,/:userid也被匹配到了,并且最后的一个NoMatch组件总是被匹配到;
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
<Route path="/:userid" component={User}/>
<Route component={NoMatch}/>
默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染;
但是实际开发中,我们往往希望有一种排他的思想:
只要匹配到了第一个,那么后面的就不应该继续匹配了;
这个时候我们可以使用Switch来将所有的Route进行包裹即可;
<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>
3.4. Redirect的使用
Redirect用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
我们这里使用这个的一个案例:
用户跳转到User界面;
但是在User界面有一个isLogin用于记录用户是否登录:
true:那么显示用户的名称;
false:直接重定向到登录界面;
App.js中提前定义好Login页面对应的Route:
<Switch>
<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高级使用
4.1. 路由嵌套
在开发中,路由之间是存在嵌套关系的。
eg:
import React, { Component } from "react";
import { NavLink, Route, Switch } from "react-router-dom";
//renderRoutes路由统一配置
import { renderRoutes } from "react-router-config";
import routes from "../router";
export function AboutHistory(props) {
return <h2>企业成立于2000年,拥有悠久的历史文化</h2>;
}
export function AboutCulture(props) {
return <h2>创新/发展</h2>;
}
export function AboutContact(props) {
return <h2>联系电话:020-888888</h2>;
}
export default class About extends Component {
render() {
console.log(this.props.route);
return (
<div>
<NavLink exact to="/about" activeClassName="about-active">
企业历史
</NavLink>
<NavLink exact to="/about/culture" activeClassName="about-active">
企业文化
</NavLink>
<NavLink exact to="/about/contact" activeClassName="about-active">
联系我们
</NavLink>
<Switch>
<Route exact path="/about" component={AboutHistory}></Route>
<Route path="/about/culture" component={AboutCulture}></Route>
<Route path="/about/contact" component={AboutContact}></Route>
<Route path="/about/join" component={AboutJoinus}></Route>
</Switch>
</div>
);
}
}
4.2. 手动跳转
目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码进行跳转。
但是通过JavaScript代码进行跳转有一个前提:必须获取到history对象。
如何可以获取到history的对象呢?两种方式
方式一:如果该组件是通过路由直接跳转过来的,那么可以直接获取history、location、match对象;
方式二:如果该组件是一个普通渲染的组件,那么不可以直接获取history、location、match对象;
如果我们希望在一个普通渲染的组件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);
withRouter的高阶函数来自react-router-dom:
使用withRouter函数,就可以获取history、location、match对象;
五、 传递参数
传递参数有三种方式:
- 动态路由的方式;
- search传递参数;
- to传入对象;
动态路由的方式
动态路由的概念指的是路由中的路径并不会固定:
比如/detail的path对应一个组件Detail;
如果我们将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示;
这个匹配规则,我们就称之为动态路由;
通常情况下,使用动态路由可以为路由传递参数。
<div>
<NavLink to="/detail/abc123">详情</NavLink>
<Switch>
<Route path="/detail/:id" component={Detail}/>
<Route component={NoMatch} />
</Switch>
</div>
detail.js的代码如下:
我们可以直接通过match对象中获取id;
这里我们没有使用withRouter,原因是因为Detail本身就是通过路由进行的跳转;
import React, { PureComponent } from 'react'
export default class Detail extends PureComponent {
render() {
console.log(this.props.match.params.id);
return (
<div>
<h2>Detail: {this.props.match.params.id}</h2>
</div>
)
}
}
search传递参数
NavLink写法:
我们在跳转的路径中添加了一些query参数;
<NavLink to="/detail2?name=lisa&age=18">详情2</NavLink>
<Switch>
<Route path="/detail2" component={Detail2}/>
</Switch>
Detail2中如何获取呢?
Detail2中是需要在location中获取search的;
注意:这个search没有被解析,需要我们自己来解析;
import React, { PureComponent } from 'react'
export default class Detail2 extends PureComponent {
render() {
console.log(this.props.location.search); // ?name=why&age=18
return (
<div>
<h2>Detail2:{this.props.location.search}</h2>
</div>
)
}
}
to传入对象
to可以直接传入一个对象
<NavLink
to={{ pathname: "/detail3", search: "?name=abc", state: info }}
activeClassName="lick-active"
>
详情3
</NavLink>
详情3
import React, { Component } from "react";
export default class Detail3 extends Component {
render() {
const location = this.props.location;
console.log(location);
return (
<div>
<h2>Detail3 : {location.state.name}</h2>
</div>
);
}
}
六. react-router-config
目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。
但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
这个时候可以使用react-router-config来完成;
安装react-router-config:
yarn add react-router-config
常见router/index.js文件:
import Home from "../pages/Home";
import About, {
AboutHistory,
AboutCulture,
AboutContact,
AboutJoinus,
} from "../pages/About";
import Profile from "../pages/Profile";
import User from "../pages/User";
import Product from "../pages/Product";
const routes = [
{
path: "/",
exact: true,
component: Home,
},
{
path: "/about",
component: About,
//子路由
routes: [
{
path: "/about",
exact: true,
component: AboutHistory,
},
{
path: "/about/culture",
component: AboutCulture,
},
{
path: "/about/contact",
component: AboutContact,
},
{
path: "/about/join",
component: AboutJoinus,
},
],
},
{
path: "/profile",
component: Profile,
},
{
path: "/user",
component: User,
},
{
path: "/product",
component: Product,
},
];
export default routes;
将之前的Switch配置,换成react-router-config中提供的renderRoutes函数:
{/* <Switch>
<Route exact path="/" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/profile" component={Profile}></Route>
<Route path="/user" component={User}></Route>
<Route path="/login" component={Login}></Route>
<Route path="/product" component={Product}></Route>
<Route path="/detail/:id" component={Detail}></Route>
<Route path="/detail2" component={Detail2}></Route>
<Route path="/detail3" component={Detail3}></Route>
<Route component={noMatch}></Route>
</Switch> */}
{renderRoutes(routes)}
如果是子组件中,需要路由跳转,那么需要在子组件中使用renderRoutes函数:
在跳转到的路由组件中会多一个 this.props.route 属性;
该route属性代表当前跳转到的路由对象,可以通过该属性获取到 routes;
export default class About extends PureComponent {
render() {
return (
<div>
<NavLink exact to="/about" activeClassName="about-active">
企业历史
</NavLink>
<NavLink exact to="/about/culture" activeClassName="about-active">
企业文化
</NavLink>
{/* <Switch>
<Route exact path="/about" component={AboutHistory}></Route>
<Route path="/about/culture" component={AboutCulture}></Route>
</Switch> */}
{/* {renderRoutes(routes[1].routes)} */}
{renderRoutes(this.props.route.routes)}
</div>
)
}
}
实际上react-router-config中还提供了一个matchRoutes辅助函数:
matchRoutes(routes, pathname)传入一个路由对象数组,获取所有匹配的路径;
const routes = matchRoutes(this.props.route.routes, "/about");
console.log(routes);