前端路由实现方式,主要有两种,分别是history和hash模式。
hash模式
不同路由对应的hash是不一样的,如何能够监听到URL中关于hash部分发生的变化?浏览器已经暴露给我们一个现成的方法hashchange
,在hash改变的时候,触发该事件。
实现示例代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing:border-box;
}
html,body {
height: 100%;
}
#content {
height: calc(100% - 50px);
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
}
#nav {
100%;
height: 50px;
position: fixed;
bottom: 0;
left: 0;
display: flex;
}
#nav a {
25%;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid black;
}
#nav a:not(:last-of-type) {
border-right: none;
}
</style>
<body>
<div id="content">内容区域</div>
<div id="nav">
<a href="#/">首页</a>
<a href="#/new-product">新品</a>
<a href="#/shopping-cart">购物车</a>
<a href="#/my">我的</a>
</div>
<script type="text/javascript">
class VueRouter {
constructor(routes = []) {
this.routes = routes;
this.currentHash = ''; // 当前hash值
window.addEventListener('load',this.refresh.bind(this),false);
window.addEventListener('hashchange',this.refresh.bind(this),false);
}
// 获取hash
getHash(url) {
return url.indexOf('#') !== -1 ? url.slice(url.indexOf('#') + 1) : '/'
}
refresh(event) {
let newHash = '';
if(event.newURL) {
newHash = this.getHash(event.newURL || '');
} else {
newHash = this.getHash(window.location.href);
}
this.currentHash = newHash;
this.matchComponentCon();
}
// 匹配hash组件内容
matchComponentCon() {
let curRoute = this.routes.find(route => route.path === this.currentHash);
if(!curRoute) {
curRoute = this.routes.find(route => route.path === '/');
}
const { component } = curRoute;
document.querySelector('#content').innerHTML = component;
}
}
const router = new VueRouter([
{path: '/',name: 'home',component: '<div>首页内容</div>'},
{path: '/new-product',name: 'new-product',component: '<div>新品内容</div>'},
{path: '/shopping-cart',name: 'shopping-cart',component: '<div>购物车内容</div>'},
{path: '/my',name: 'my',component: '<div>我的内容</div>'}
])
</script>
</body>
</html>
效果如图所示:
当点击底部不同的菜单,中间区域显示对应菜单的内容。
history模式
HTML5提供的一个history全局对象,这个对象包含了关于我们访问网页(历史会话)的一些信息,history路由的实现要归功于它。
history它还暴露了一些方法,如:
- window.history.go: 可以跳转到浏览器会话历史中的指定的某一个记录页
- window.history.forward: 指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同
- window.history.back: 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同
- window.history.pushState: 可以将给定的数据压入到浏览器会话历史栈中
- window.history.replaceState: 将当前的会话页面的url替换成指定的数据
而history路由的实现,主要就是依靠于pushState与replaceState实现的。这两个方法的特点:
- 改变当前页面的URL,不会刷新页面;
- 调用
pushState
方法会把当前的URL压入到浏览器的会话历史栈中,会让history.length加1,而replaceState
是替换当前的这条会话历史,不会增加history.length;
我们是否可以通过pushState
或replaceState
能够监听URL变化这个动作,就可以实现不同路由页面的渲染处理,我们需要了解下事件处理程序popState
,官网参考地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event
每当激活同一文档中不同的历史记录条目时,popstate
事件就会在对应的window
对象上触发。如果当前处于激活状态的历史记录条目是由history.pushState()
方法创建的或者是由 history.replaceState()
方法修改的,则 popstate
事件的 state
属性包含了这个历史记录条目的 state
对象的一个拷贝。
备注: 调用
history.pushState()
或者history.replaceState()
不会触发popstate
事件。popstate
事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用history.back()
方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。
从官网描述看,总结如下:
history.pushState
和history.replaceState
方法不会触发popstate事件- 浏览器的某些行为会导致
popstate
事件,比如go、back、forward
popstate
事件对象中的state
属性,可以理解是我们在通过history.pushState
或history.replaceState
方法时,传入的指定的数据
结论就是popstate无法监听history.pushState
和history.replaceState
方法,既然不支持,那么我们重新写下这个history.pushState
和history.replaceState
方法。在这个方法中,也能够暴露出自定义的全局事件,然后再监听自定义的事件
就行了。
let _wr = function(type) {
let orig = history[type];
return function() {
orig.apply(this,arguments);
let e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
}
}
history.pushState = _wr('pushState');
history.replaceState = _wr('replaceState');
执行上面的方法,相当于自定义了同名的pushState
和replaceState
自定义事件的触发绑定到了window上面,即是可以通过pushSate
或者replaceState
来执行自定义的事件,同时还执行了浏览器的history.pushState
或者history.replaceState
方法。
简单实现:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing:border-box;
}
html,body {
height: 100%;
}
#content {
height: calc(100% - 50px);
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
}
</style>
<body>
<div id="content">内容区域</div>
<button id="button1">首页</button>
<button id="button2">新品</button>
<button id="button3">购物车</button>
<button id="button4">我的</button>
<script type="text/javascript">
const button1 = document.querySelector('#button1');
const button2 = document.querySelector('#button2');
const button3 = document.querySelector('#button3');
const button4 = document.querySelector('#button4');
let _wr = function(type) {
let orig = history[type];
return function() {
orig.apply(this,arguments);
let e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
}
}
history.pushState = _wr('pushState');
history.replaceState = _wr('replaceState');
button1.addEventListener('click',() => {
const path = './home';
const title = '首页';
history.pushState({ path,title },title, path);
})
button2.addEventListener('click',() => {
const path = './new-product';
const title = '新品';
history.pushState({ path,title },title, path);
})
button3.addEventListener('click',() => {
const path = './shopping-cart';
const title = '购物车';
history.pushState({ path,title },title, path);
})
button4.addEventListener('click',() => {
const path = './my';
const title = '我的';
history.pushState({ path,title },title, path);
})
class VueRouter {
constructor(routes = []) {
this.routes = routes;
this.currentUrl = '';
this.matchComponentCon();
window.addEventListener('pushState', e => {
console.log(e)
this.currentUrl = e.arguments[2] && e.arguments[2].slice(e.arguments[2].indexOf('.') + 1);
window.document.title = e.arguments[1];
this.matchComponentCon();
},false)
// 监听浏览器的back、forward、go事件
window.addEventListener('popstate', e => {
const { path = '/', title } = e.state || {};
console.log(e)
this.currentUrl = path.slice(path.indexOf('.') + 1);
window.document.title = title;
this.matchComponentCon();
},false)
}
// 匹配路由页面内容
matchComponentCon() {
let curRoute = this.routes.find(route => route.path === this.currentUrl);
if(!curRoute) {
curRoute = this.routes.find(route => route.path === '/');
}
const { component } = curRoute;
document.querySelector('#content').innerHTML = component;
}
}
const router = new VueRouter([
{path: '/',name: 'home',component: '<div>首页内容</div>'},
{path: '/new-product',name: 'new-product',component: '<div>新品内容</div>'},
{path: '/shopping-cart',name: 'shopping-cart',component: '<div>购物车内容</div>'},
{path: '/my',name: 'my',component: '<div>我的内容</div>'}
])
</script>
</body>
</html>
history模式需要后端配合解决刷新页面404的问题。nginx配置如下:
location / {
try_files $uri $uri/ /index.html;
}