一、路由的概念
在网络原理中,路由指的是分组从源到目的地时,决定端到端路径的网络范围的进程,做成硬件叫路由器,在路由器中维护着一个路由表,并根据此路由表决定下一跳的地址。在web应用中,路由实际是指根据不同的url给其分配不同的控制器(处理程序)。
二、前端路由的出现
最初在web应用开发中前端并不太关注路由,这和当时的开发方式和业务处理有关,采用的是后端模板渲染的方式。我们常看到的jsp,php都是这种方案,由后端根据url请求信息来决定响应某个页面,此时路由是在服务端配置的。这时候的路由就是url和后端服务器的交互,根据不同的路径显示不同的资源,页面也是一种资源。
这种开发方式有明显的不足,每切换一个页面都要重新加载一次,即使两个页面有很多相同的地方。还有就是前后端的代码揉杂在一起,前端要部署一个既有前端代码又有后端代码的项目,不方便本地开发调试,一旦后端代码有错误,前端无法进行开发,前端被限制在后端的开发方式中,效率很低,前端迫切的需要一种革新来改变这种开发方式。
随着前后端分离和MVVM概念的兴起及前端工程化的发展,出现了一种新的开发方式,单页应用(SPA),前端圈迅速崛起,有了爆发式的发展。单页应用的意思是只有一个页面,是无刷新的,看到的页面之间的跳转其实只是组件的切换,同时URL也要相应的变化,为了实现这种单页应用,出现了前端路由。
三、前端路由的核心实现
前端路由的核心就是URL和组件树的映射关系。因为单页应用前端整个工程实际上只有一个页面,不同的url只是在切换不同的组件,实际上就是监听url的变化然后按照他的规则来进行匹配。前端路由的实现方式主要有两种: hash
和 history
模式。
1. hash模式
即 window.loacation.hash
,url中以“#”为标识符,如:http://www.xxx/com/list.html#complete ,这个值可读可写,读取时,可以用来判断网页状态,写入时会在不重新载入网页的情况下给浏览器增加一条历史记录,有了这种特性就有了前端路由的雏形,因为改变#之后的内容相当于改变了url,但是并没有重新向服务器发送请求。JavaScript可以通过window.onhashchange来监听url变化,以实现不同组件的切换。
目前主要的路由库有vue-router,react-router,他们的主要功能是存储路由的hash以及对应的函数,然后监听hash的变化执行对应的函数。以vue-router为例,看一下他的监听源码:
setupListeners () { //设置监听器
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) { // hash变化改变view
replaceHash(route.fullPath)
}
})
})
}
这里他把hashchange是作为降级方案处理的,因为有更优的处理方式,下面会讲。这里只是设置监听的代码,当然他的前后还有一些“钩子”,即跳转前,跳转后要执行的方法,甚至有可能取消此次跳转。
2. history模式
由于html5的发布,引入了 history.pushState()
和 history.replaceState()
方法,它们分别可以添加和修改历史记录条目。pushState需要三个参数,一个状态对象(可以通过onpopstate事件获取到),一个标题 (目前被忽略)和一个URL,replaceState参数也是一样。
通常 与window.onpopstate
配合使用,这个为前端路由的另一种模式奠定了基础,但是这种方式的url是一个完整的如http://www.xxx.com/list/complete,他每一次改变都会向服务发送一次请求资源(其实我们是没有这个页面地址的),所以需要服务器端增加一条配置,如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是我们的主页面。
下面我们用此种方法做一个简版的路由,开发思想涉及到了观察者模式,都是考虑最简单的情况。
<!--index.html-->
<div class="nav" id="nav">
<a href="javascropt:;" onclick="route.go('/')">首页</a>
<a href="javascropt:;" onclick="route.go('/list')">列表页面</a>
<a href="javascropt:;" onclick="route.go('/cart')">购物车页面</a>
<a href="javascropt:;" onclick="route.go('/home')">我的</a>
</div>
<div id="content"></div>
<script src="./router.js"></script>
// router.js
class Router{
constructor(routes){
this.routes = {};
this.init(routes);
this.onPopState(); //监听popstate事件
}
init(routes){ //初始化路由,并设置监听,
let _this = this;
Object.keys(routes).forEach((item,index,array)=>{
_this.listen(item,routes[item]);
});
this.go("/"); //或个元素者取出routes对象的第一个元素
}
listen(url,fn){ //这里假设只有一个方法,不是方法数组添加一条
this.routes[url] = fn;
}
go(url){ //触发切换事件,这里假设只有一个方法,多个方法需要循环取出并执行,并且需要处理参数情况
history.pushState({url},null,url); //push进去一条历史记录
this.routes[url] && this.routes[url]();
}
onPopState(){ //处理浏览器前进后退事件
let _this = this;
window.addEventListener("popstate",e=>{
let url = e.state && e.state.url;
console.log(url);
_this.routes[url]&&_this.routes[url]();
});
}
}
const route = new Router({ //
"/":()=>render("首页内容"),
"/list":()=>render("列表页内容"),
"/cart":()=>render("购物车内容"),
"/home":()=>render("我的内容"),
});
function render(content){
document.getElementById("content").innerHTML = content;
}
总结
目前web前端路由实现的整体思路是一样的,将url映射到组件,再加上一系列的复杂情况的处理,比如说 hash模式
和 history模式
如何兼容,重定向,别名,嵌套,传参,跳转及跳转时需要提供的各种“钩子”,处理好以上各种情况就是一个完整的前端路由库了。