单页应用(SPA)越来越成为前端主流。一大特点是使用前端路由:跳转逻辑由前端直接控制,不再完全交给后端,前端也因此更自由。
实现上主要有两种:hash 模式和history 模式。前者靠 URL 里 # 后面的片段,后者靠 HTML5 History API,看起来更像普通路径。
| 对比 |
hash |
history |
| URL 形态 |
#plan 这种片段 |
/plan/index 这种路径 |
| 改 URL 会不会请求后端 |
不会,只触发 hashchange |
pushState / replaceState 本身不请求 |
| 刷新 |
一般没问题 |
后端必须把所有路径指到 index.html,否则 404 |
| 兼容 |
IE8+ |
IE10+ |
一、hash 模式
做锚点跳转时就能见到 URL 后面跟 #id。hash 就是从 # 开始到结束的那一段。纷享销客就是 hash 模式:
//纷享销客系统采取的就是hash模式
https://www.fxiaoke.com/XV/Home/Index#plan
https://www.fxiaoke.com/XV/Home/Index#approve
https://www.fxiaoke.com/XV/Home/Index#crm/index
hash 变化浏览器不会重新发请求,但会触发 window.hashchange(原文示例里写作 hashChange)。在这个事件里读当前 hash 并改页面内容,就达到了前端路由的目的。
实现原理
//打开浏览器控制台执行如下代码
window.onhashchange = () => {
console.log('hash :',location.hash);
switch (location.hash) {
case '#index':
console.log('index>>>>');
break
case '#news':
console.log('news>>>>');
break
default:
console.log('404>>>>');
}
}
location.hash = '#index'
location.hash = '#news'
location.hash = '#other'
用 hash 切换页面
HTML:菜单 href 写成 hash,id 为 app 的容器里放页面内容。
<ul id="menu">
<li>
<a href="#index">首页</a>
</li>
<li>
<a href="#news">资讯</a>
</li>
<li>
<a href="#user">个人中心</a>
</li>
</ul>
<div id="app"></div>
JS:在 window.onhashchange 里取 hash,按不同值改 app 内容。
function hashChange(e){
// console.log(location.hash)
// console.log(location.href)
//
// console.log(e.newURL)
// console.log(e.oldURL)
let app = document.getElementById('app')
switch (location.hash) {
case '#index':
app.innerHTML = '<h1>这是首页内容</h1>'
break
case '#news':
app.innerHTML = '<h1>这是新闻内容</h1>'
break
case '#user':
app.innerHTML = '<h1>这是个人中心内容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
window.onhashchange = hashChange
hashChange()
这样就实现了一个简单的 hash 路由。
二、history 模式
hash 看起来比较丑,都带着 #。history 模式是平时看到的正常连接形式:
https://47.109.74.84#plan/index //hash模式路由
https://47.109.74.84/plan/index //history模式路由
它基于 window.history。HTML4 里已经能控制历史记录跳转,常用方法包括:
history.forward(); //在历史记录中前进一步
history.back(); //在历史记录中后退一步
history.go(n): //在历史记录中跳转n步骤,n=0为刷新本页,n=-1为后退一页。
在HTML5中,window.history对象得到了扩展,新增的API包括:
history.pushState(data[,title][,url]);//向历史记录中追加一条记录
history.replaceState(data[,title][,url]);//替换当前页在历史记录中的信息。
history.state;//是一个属性,可以得到当前页的state信息。
window.onpopstate;//是一个事件,在点击浏览器后退按钮或js调用forward()、back()、go()时触发。监听函数中可传入一个event对象,event.state即为通过pushState()或replaceState()方法传入的data参数
原理可以这样理解:给超链接加 onclick,阻止默认跳转,改用 history.pushState 或 replaceState 改浏览器 URL,同时改页面内容。History API 本身不会向后端发请求,于是也实现了前端路由。
用户点浏览器前进后退会触发 window.onpopstate,监听后再按路由改内容。菜单也不一定是超链接,任意元素在点击里调 history 即可。
实现原理
//浏览器控制台执行下列测试代码
window.onpopstate = (e) => {
const path = location.pathname;
console.log('path :',path);
switch (path) {
case '/index':
console.log('这是首页内容')
break
case '/news':
console.log('这是新闻内容')
break
default:
console.log('404找不到资源')
}
}
/**
使用方法
pushState(state, title, url)
参数说明
state: 可以通过history.state读取
title: 可选参数,暂时没有用,建议传个短标题
url: 改变过后的url地址
*/
history.pushState({page: 1}, "title 1", "/index");
window.history.pushState({page:1},'','https://it.zhousir.top/index')
popstate 事件
用户点后退、前进,或 JS 调用 history.back()、history.go()、history.forward() 会触发 popstate;pushState、replaceState 本身不会。若要监听后两者的变化,可参考 如何监听URL的变化?。
window.addEventListener('pushState', function(e) {
console.log('THEY DID IT AGAIN! pushState 2222222');
});
history 示例
HTML:
<ul id="menu">
<li>
<a href="/index">首页</a>
</li>
<li>
<a href="/news">资讯</a>
</li>
<li>
<a href="/user">个人中心</a>
</li>
</ul>
<div id="app"></div>
JS:改造超链接,阻止默认跳转(默认跳转会刷新页面)。
//改造超链接,阻止默认跳转,默认的跳转是会刷新页面的
document.querySelector('#menu').addEventListener('click',function (e) {
if(e.target.nodeName ==='A'){
e.preventDefault()
let path = e.target.getAttribute('href') //获取超链接的href,改为pushState跳转,不刷新页面
window.history.pushState({},'',path) //修改浏览器中显示的url地址
render(path) //根据path,更改页面内容
}
})
function render(path) {
let app = document.getElementById('app')
switch (path) {
case '/index':
app.innerHTML = '<h1>这是首页内容</h1>'
break
case '/news':
app.innerHTML = '<h1>这是新闻内容</h1>'
break
case '/user':
app.innerHTML = '<h1>这是个人中心内容</h1>'
break
default:
app.innerHTML = '<h1>404</h1>'
}
}
//监听浏览器前进后退事件,并根据当前路径渲染页面
window.onpopstate = function (e) {
render(location.pathname)
}
//第一次进入页面显示首页
render('/index')
上面写法比较初级,可以用类封装:add 添加路由,pushState 跳转,初始化时改掉所有超链接的跳转方式。这个 demo 不能直接用于生产,只为加深理解。
三、两种模式的区别
-
-
pushState 的新 URL 可以是同源任意 URL;hash 只能改 # 后面,只能是当前文档内的 URL
-
pushState 即使新 URL 和当前一模一样,也会入栈;hash 必须和新值不同才会入栈
-
pushState 可通过 stateObject 往记录里放任意类型数据;hash 只能加短字符串
-
pushState 可额外设置 title 供后续使用
-
hash 兼容 IE8 以上,history 兼容 IE10 以上
-
history 需要后端把所有访问指到 index.html,否则用户刷新会 404
一句话总结:hash 改的是 # 后面、不依赖后端;history 更美观,但刷新必须由服务端兜底到同一份 SPA。