前沿知识:
History 接口方法:
1.back():与用户当即浏览器的Back按钮行为相同;等价于 history.go(-1)。会触发页面改变。
2.forward():与用户当即浏览器的Forward按钮行为相同;等价于 history.go(1)。会触发页面改变。
3.go():会触发页面改变。
4.pushState() : 按指定的名称和 URL(如果提供该参数)将数据 push 进会话历史栈;只是改变会话历史栈,不会触发页面改变。URL改变,但是不会加载新页面。
- 调用 pushState() 和 window.location = "#foo"基本上一样,他们都会在当前的 document 中创建和激活一个新的历史记录;
不会触发 hashchange 事件,即使新的 URL 与旧的 URL 仅哈希不同也是如此。
5.replaceState(): 按指定的数据、名称和 URL(如果提供该参数),更新 history 栈上最新的条目。只是改变会话历史栈,不会触发页面改变。URL改变,但是不会加载新页面。
Window事件
- onpopState():
调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,页面改变时才会触发。比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。 - onhashchange: hash值改变时触发,
不会触发页面改变。
hash模式
即改变url中的hash值部分,hash改变的时候会触发onhashchange事件。改变的方式有:
- js编程方式:window.location.hash修改
- a标签的href挂上hash值,通过a标签跳转
- 浏览器前进、后退按钮
我们要实现hash模式页面渲染,那么只要实现这三种方式都可以渲染页面即可,正好三种方式都可以触发onhashchange事件,因此我们只要在onhashchange事件中实现渲染页面,就可以实现hash模式的路由了。 接下来我们通过代码的方式将三种方式实现,其中js编程方式通过button按钮绑定点击事件来实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hash模式实现router</title>
<style>
html, body {
height: 100%;
}
</style>
</head>
<body>
<div>
<div class="nav">
<ul>
<li><a href="#/">page1</a></li>
<li><a href="#/page2">page2</a></li>
<li><a href="#/page3">page3</a></li>
</ul>
<button onclick="go('#/')">page1</button>
<button onclick="go('#/page2')">page2</button>
<button onclick="go('#/page3')">page3</button>
</div>
<div id="router-view"></div>
</div>
</body>
<script type="text/javascript">
// button是通过编程方式改变hash
function go(path) {
window.location.hash = path
}
class Router {
constructor(routes = []) {
this.routes = routes
// 页面第一次加载不会触发hashchange事件,手动渲染页面
window.addEventListener('load', this.render.bind(this), false)
window.addEventListener('hashchange', this.render.bind(this), false)
}
// 获取hash值
getHash() {
const hash = window.location.hash
return hash ? hash.slice(1) : '/'
}
// 渲染对应页面
render() {
let curPath = this.getHash()
curPath = curPath.includes('/') ? curPath : '/'+curPath
let curRoute = this.routes.find(route => route.path === curPath)
if(!curRoute) {
curRoute = this.routes.find(route => route.path === '/')
}
const view = document.getElementById('router-view')
view.innerHTML = curRoute.component
}
}
const routes = new Router([
{
path: '/',
name: 'page1',
component: `<div>page1</div>`
},
{
path: '/page2',
name: 'page2',
component: `<div>page2</div>`
},
{
path: '/page3',
name: 'page3',
component: `<div>page3</div>`
}
])
</script>
</html>
Tips:
在Router的构造器中通过addEventListener监听load和hashchange事件。由于是绑定到window上的,但是绑定的函数都是Router类中的,故需要通过bind绑定this,防止在window上找不到render方法。
history模式
修改url的方式:
- js编程方式:histroy.pushState修改
- a标签跳转 => 拦截默认跳转,防止页面刷新,使用histroy.pushState修改url
- 浏览器前进、后退按钮 => 触发onpopstate事件
接下来我们通过代码将以上三种方式实现,其中js编程方式是通过button按钮绑定点击事件,a标签方式需要拦截默认跳转,因为:
histroy模式的 URL没有 # 分隔符。我们知道,改变url中的hash部分不会刷新页面,也不会向后台发送数据。但是改变url非hash值部分会自动刷新页面。故,当我们通过a标签改变URL的时候,需要拦截浏览器跳转行为。Histroy API正好提供了pushState方法,可以改变url还不刷新页面,类似于修改hash的形式。因此,histroy模式我们是借用pushState方法来完成的。
总结:histroy.pushState只可以改变路由,但是并无法刷新页面,我们利用的就是histroy.pushState改变路由却不刷新页面的方式来实现histroy模式的路由,渲染页面的操作是我们手动完成的。
因此,我们最终需要通过两种方式渲染histroy模式的路由:
- histroy.pushState改变路由,然后渲染页面。
- onpopstate事件中渲染页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>histroy模式</title>
</head>
<body>
<ul>
<li><a href="./">page1</a></li>
<li><a href="./page2">page2</a></li>
<li><a href="./page3">page3</a></li>
</ul>
<button onclick="go('page1')">page1</button>
<button onclick="go('page2')">page2</button>
<button onclick="go('page3')">page3</button>
<div id="router-view"></div>
</body>
<script>
class Router {
constructor(routes) {
this.routes = routes
window.addEventListener('load', this.load.bind(this))
window.addEventListener('hashchange', this.render.bind(this))
}
getPath() {
const path = window.location.href.split('/')
return path ? path.slice(-1) : '/'
}
go(path) {
history.pushState(null, '', path)
this.render()
}
load() {
const aList = document.querySelectorAll('a[href]')
aList.forEach((aNode) =>
aNode.addEventListener('click', (e) => {
e.preventDefault()
// 拦截a标签点击事件,手动通过pushState的方式改变url,防止页面刷新,并手动渲染
const href = aNode.getAttribute('href')
history.pushState(null, '', href)
this.render()
})
)
this.render()
}
render() {
const curPath = this.getPath()
let curRoute = this.routes.find((route) => route.path === '/' + curPath)
if (!curRoute) {
curRoute = this.routes.find((route) => route.path === '/')
}
const view = document.getElementById('router-view')
view.innerHTML = curRoute.component
}
}
const routes = new Router([
{
path: '/',
name: 'page1',
component: `<div>page1</div>`,
},
{
path: '/page2',
name: 'page2',
component: `<div>page2</div>`,
},
{
path: '/page3',
name: 'page3',
component: `<div>page3</div>`,
},
])
// 编程方式改变路由
function go(path) {
routes.go(path)
}
</script>
</html>
Tips:
histroy.pushState方法不能在静态文件中使用,需要通过web服务启动使用,比如使用Live Server启动本地文件。