前言
被业务追赶着的,一下子停更了一个月(不会有人认为是偷懒没写吧不会吧),最近的重构工作到了整理路由的这一块,用到了router4整体,就整理了一下路由相关内容,路由可以说是我们接触到最多的东西,当然不能只会用了。
脑图🔽
路由是啥捏
司尘(抢先回答):就是实现浏览器在不刷新界面的情况下,切换界面。
机智的读者:这只是前端路由实现的效果,特地整理当然要详细咯
路由概念
路由就是url到界面的一个映射,这样一想我们每个页面去请求也可以实现路由,所以路由分两种
服务器端路由
类似这种:https://www.abc.com/index.html,直接给你返回一个页面,他的过程当然就是网络请求页面的过程了:输入url浏览器干了啥?客户端路由(前端路由)
要解决后端路由的请求频繁问题那么我们就要实现- 改变url浏览器不发请求
- 在不刷新界面的情况下改变url
- 可以监听到url的变化
司尘:这么复杂咋解决嘛?
机智的读者:前端这不给了两种模式嘛都给你实现了
司尘:哦那没事了
前端路由方式
hash模式
跳转实现
location对象中的hash值可以让帮助实现
直接改变location.hash的值
通过
a标签实现
<a href="#login">edit</a>
监听hash
js提供给我们两种方式监听hash变化
window.onhashchange = function(hash) {
console.log(hash)
}
window.addEventListener('hashchange', function(hash) {
console.log(hash)
})
hash劣势
为啥早期使用hash而现在基本都没有地方使用了呢?能想到的他的优点也就只有兼容性好了
最直观的,多个
#看着就是难受,那我不管真的感觉丑(把肤浅打在公屏上)配合后端困难 对于部分需要重定向的操作,后端无法获取hash部分内容,导致后台无法取得url中的数据
服务器端无法准确跟踪前端路由信息
对于需要锚点功能的需求会与目前路由机制冲突
history模式
这个模式就是基于HTML5的History接口,理所当然兼容性会差一点。 我们路由场景就只有:前进、后退、指定到某个页面。那么对应到的方法我们就落列一下:
- History.forward()
- History.back()
- History.go()
- History.pushState()
- History.replaceState()
这三种方法看起来就好像是在操作一个已有的路由对象,我们初次进来怎么会有嘛,所以有history.pushState方法,点击路由跳转就会去往堆栈里面添加路由信息,History.forward()、History.back()、History.go() 这三个方法使用时会调用popState方法,在堆栈中进行操作,所以也不会去刷新界面
React-router4.0
到今天的正题至于他的使用以及API这边就不做介绍了,官方文档里都有,我们直接来通过源码看一下,冲!
React-router中将上面的History类的构建方法,独立成一个node包,包名为history,路由的实现history完全继承了History接口,因此拥有History中的所有的属性和方法。
先来一个简单的demo
class Square extends React.Component {
render() {
return (
<BrowserRouter>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/page">page</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/page" component={Page}/>
</Switch>
</div>
</BrowserRouter>
)
}
}
路由相关信息方法肯定是在props上的我们打印看一下,这是我的两次操作
- 点击跳转
- 浏览器回退键
{
history: {...}, // history库提供的方法
match: {
isExact:true // 是否为严格匹配
path: "/page", // 用来匹配的
url: "/page", // 当前的URL
params: {}, // 路径中的参数
},
location: {
hash: "" // hash
key: "lm9hdk" // 唯一的key
pathname: "/page" // URL 中路径部分
search: "" // URL 参数
state: undefined // 路由跳转时传递的参数 state
}
staticContext: undefined // 用于服务端渲染
}
BrowserRouter和HashRouter
代码看来BrowserRouter就是注册了history然后能够让子路由内容全部能获取到history,HashRouter其实也大同小异。
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
这样看来我们要追溯到Router组件了
Router组件
比作是一个监听器我们这样去理解会好一些,每一次改变我们通过Router组件的监听然后去改变props里面的值
function getContext(props, state) {
return {
history: props.history,
location: state.location,
match: Router.computeRootMatch(state.location.pathname),
staticContext: props.staticContext
};
}
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
this._isMounted = false;
this._pendingLocation = null;
// staticContext为true时,为服务器端渲染那就不需要前端去操作路由咯
if (!props.staticContext) {
// 监听listen,location改变触发
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
// 更新 location
this.setState({ location });
} else {
// 否则存储到_pendingLocation, 等到didmount再setState避免可能报错
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
// 赋值为true,且不会再改变
this._isMounted = true;
// 更新location
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
// 取消监听
if (this.unlisten) this.unlisten();
}
render() {
const context = getContext(this.props, this.state);
return (
<RouterContext.Provider
children={this.props.children || null}
value={context}
/>
);
}
}
Route组件
当url改变的时候,将path属性与改变后的url做对比,如果匹配成功,则渲染该组件的componet或者children属性所赋值的那个组件
class Route extends React.Component {
...
componentWillReceiveProps(nextProps, nextContext) {
...
this.setState({
match: this.computeMatch(nextProps, nextContext.ro uter)
});
}
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const { history, route, staticContext } = this.context.router;
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children))
return React.Children.only(children);
return null;
}
}
Switch
前面的这几个组件好像已经是实现了路由,那我们为啥要在Route组件外面包裹一个Switch呢?有点累了看一眼女友
按照路由匹配规则,URL是/page 那么Home组件也会被渲染出来。
Switch 是用来嵌套在 Route 的外面,当 Switch 中的第一个 Route 匹配之后就不会再渲染其他的 Route 了,我们直接把他理解为一个精准匹配路由的工具这样比较好理解
render() {
const { route } = this.context.router;
const { children } = this.props;
const location = this.props.location || route.location;
let match, child;
React.Children.forEach(children, element => {
if (match == null && React.isValidElement(element)) {
const {
path: pathProp,
exact,
strict,
sensitive,
from
} = element.props;
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
return match
? React.cloneElement(child, { location, computedMatch: match })
: null;
}
Switch是通过matchPath这个函数来判断是否匹配成功,这个方法就是做了判断 location 是否符合 path。
Link
最后那就是实现我们路由跳转的Link组件了,想想他就不会复杂,就是去调了一下history的方法嘛,我们一起康康
class Link extends React.Component {
// 定义一个点击方法
handleClick = event => {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // 组织违约
event.button === 0 && // 忽略一切,除了左键点击
!this.props.target && // 让浏览器处理"target=_blank"等
!isModifiedEvent(event) // 忽略点击修改键
) {
event.preventDefault();
const { history } = this.context.router;
const { replace, to } = this.props;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
};
render() {
const { replace, to, innerRef, ...props } = this.props;
const { history } = this.context.router;
const location =
typeof to === "string"
? createLocation(to, null, null, history.location)
: to;
const href = history.createHref(location);
// 最终创建的是一个 a 标签
return (
<a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
);
}
}
总结
自己之前还只是会去用,甚至switch这种组件都不知道是干嘛的就想着要写在那里,看了具体实现之后其实才懂得他的整个流程,走了一遍还是清晰了很多。最近碰到的一个登陆加密问题下一篇想写一下https为啥不会被攻击,加油肝!