背景
目前单页应用(SPA)越来越成为前端主流,单页应用一大特点就是使用前端路由,由前端来直接控制路由跳转逻辑,而不再由后端人员控制,这给了前端更多的自由。
目前前端路由主要有两种实现方式:hash 模式和 history模式,下面分别详细说明。
hash模式
hash模式是基于location.hash
来实现的,通过onhashchange事件
监听hash改变,从而对页面进行跳转(渲染)
比如下面这个网站,它的 location.hash 的值为 '#home':
https://www.test.com#home
特点
- URL中的hash值只是客户端的一种状态,服务器发出请求时,hash部分不会被发送
- hash值的改变,都会在浏览器历史中增加一个记录,因此我们可以通过浏览器的前进、后退控制hash的切换
- 可以通过hashchange事件来监听hash的变化
history模式
history模式通过H5中History API
来实现url的变化。主要通过 pushState()和replaceState()
这两个api结合popstate事件
实现页面跳转(渲染)
history 模式与原url无异,不带#字符,比如下面这个网站
https://www.test.com/home
特点
- pushState、repalceState 两个API来操作实现URL的变化
- 通过popstate事件来监听url变化
- pushState、replaceState不会触发popstate事件,需要手动触发页面跳转
区别:
- hash模式,#后的信息不会包含在http请求中,history需要后端进行适配,否则刷新会访问真实url
- 兼容性:hash兼容IE8以上、history兼容IE10以上
- 描述性:hash只能添加字符串,history可以添加title、state等描述对象
- 局限性:hash只能改变#后的字符串,history模式可以修改同源的任意URL
进阶
实现一个Router类,通过add方法添加路由配置,第一个参数为路由路径,第二个参数为render函数,返回要插入页面的html;通过listen方法,监听hash变化,并将每个路由返回的html,插入到app中。
以下分别通过hash模式与history模式进行实现
手写hash模式
class Router {
constructor(){
this.routers = [] //存放我们的路由配置
}
add(route,callback){
this.routers.push({
path:route,
render:callback
})
}
listen(callback){
window.onhashchange = this.hashChange(callback)
this.hashChange(callback)() //首次进入页面的时候没有触发hashchange,必须要就单独调用一下
}
hashChange(callback){
let self = this
return function () {
let hash = location.hash
for(let i=0;i<self.routers.length;i++){
let route = self.routers[i]
if(hash===route.path){
callback(route.render())
return
}
}
}
}
}
let router = new Router()
router.add('#index',()=>{
return '<h1>这是首页内容</h1>'
})
router.add('#news',()=>{
return '<h1>这是新闻内容</h1>'
})
router.add('#user',()=>{
return '<h1>这是个人中心内容</h1>'
})
router.listen((renderHtml)=>{
let app = document.getElementById('app')
app.innerHTML = renderHtml
})
手写history模式
class Router {
constructor(){
this.routers = []
this.renderCallback = null
}
add(route,callback){
this.routers.push({
path:route,
render:callback
})
}
pushState(path,data={}){
window.history.pushState(data,'',path)
this.renderHtml(path)
}
listen(callback){
this.renderCallback = callback
this.changeA()
window.onpopstate = ()=>this.renderHtml(this.getCurrentPath())
this.renderHtml(this.getCurrentPath())
}
changeA(){
document.addEventListener('click', (e)=> {
if(e.target.nodeName==='A'){
e.preventDefault()
let path = e.target.getAttribute('href')
this.pushState(path)
}
})
}
getCurrentPath(){
return location.pathname
}
renderHtml(path){
for(let i=0;i<this.routers.length;i++){
let route = this.routers[i]
if(path===route.path){
this.renderCallback(route.render())
return
}
}
}
}
let router = new Router()
router.add('/index',()=>{
return '<h1>这是首页内容</h1>'
})
router.add('/news',()=>{
return '<h1>这是新闻内容</h1>'
})
router.add('/user',()=>{
return '<h1>这是个人中心内容</h1>'
})
router.listen((renderHtml)=>{
let app = document.getElementById('app')
app.innerHTML = renderHtml
})