原文链接:
前述
前端三大框架Angular
、React
和Vue
都推行单页面应用SPA开发模式,这是因为在路由切换时,替换DOM Tree中发生修改的DOM部分,来减少原来因为多页面应用跳转带来巨大的性能损耗。
他们都有自己典型的路由解决方案:@Angular/router、react-router、vue-router
。
一般来说,这些路由插件总是提供俩种不同的路由方式:Hash
和History
,有时候也会提供非浏览器环境下的路由方式Abstract
,在vue-router
中使用外观模式将不同的几种路由方式提供了一个一致的高层接口,让我们可以在不同路由方式中切换。
Hash 和 History 除了外观上的不同之外。还有一个重要的区别:Hash方法的状态保存需要另行传递,而HTML5 History原生提供了自定义状态传递的能力,我们可以直接利用其来传递信息。
1.Hash
1.1 相关api
MDN:Location
MDN上的例子:
var url = document.createElement('a');
url.href = 'https://developer.mozilla.org/en-US/search?q=URL#search-results-close-container';
console.log(url.href); // https://developer.mozilla.org/en-US/search?q=URL#search-results-close-container
console.log(url.protocol); // https:
console.log(url.host); // developer.mozilla.org
console.log(url.hostname); // developer.mozilla.org
console.log(url.port); // (blank - https assumes port 443)
console.log(url.pathname); // /en-US/search
console.log(url.search); // ?q=URL
console.log(url.hash); // #search-results-close-container
console.log(url.origin); // https://developer.mozilla.org
注意: Hash方法利用了相当于页面锚点的功能,所以与原来的通过锚点定位来进行页面滚动定位的方法冲突,导致定位到错误的路由路径,因此需要采用别方法。
1.2 实例
原理是吧目标路由和对应的回调记录下来,点击跳转触发hashchange
的时候获取当前路径并执行对应的回调,效果:
<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<title>Document</title>
</head> <body>
<ul>
<li><a href="#/">/</a></li>
<li><a href="#/page1">/page1</a></li>
<li><a href="#/page2">/page2</a></li>
</ul>
<div class="content-div"></div>
<script>
// 创建路由类
class RouterClass {
constructor() {
this.routes = {} // 记录路径标识符对应的 cb
this.currentUrl = '' // 记录hash只为方便执行 cb
window.addEventListener('load', () => this.render())
window.addEventListener('hashchange', () => this.render())
} // 初始化
static init() {
window.Router = new RouterClass()
} // 注册路由 和 回调 @param - path - 路径
// @param - cb - 回调
route(path, cb) {
// 将路径及其对应的方法添加到 this.routes 对象中
this.routes[path] = cb || function() {}
} // 记录当前 hash,执行cb
render() {
this.currentUrl = location.hash.slice(1) || '/'
this.routes[this.currentUrl]() // 默认页面
}
} // 调用方法,监听 load 和 hashchange 事件
RouterClass.init()
// 过去div 并 给div中添加数据
const ContentDom = document.querySelector('.content-div')
const changeContent = content => ContentDom.innerHTML = content // 调用方法
Router.route('/', () => changeContent('默认页面'))
Router.route('/page1', () => changeContent('page1页面'))
Router.route('/page2', () => changeContent('page2页面'))
</script>
</body> </html>
如果希望使用脚本来控制Hash路由的后退,可以将经历的路由路径记录下来,路由后退跳转的实现是对location.hash
进行赋值。但是这样会引发重新hashchange
事件,第二次进入render
。所以我们需要增加一个标志位,来标明进入render
方法是因为回退进入的还是用户跳转进入的。
<!DOCTYPE html>
<html lang="en"> <head>
<meta charset="UTF-8">
<title>Document</title>
</head> <body> <ul>
<li><a href="#/">/</a></li>
<li><a href="#/page1">page1</a></li>
<li><a href="#/page2">page2</a></li>
</ul>
<div class='content-div'></div> <button>back</button> <script>
class RouterClass {
constructor() {
this,isBack = false
this.routes = {} // 记录路径标识符对应的cb
this.currentStack = [] // hash 栈
window.addEventListener('load', () => this.render())
window.addEventListener('hashchange', () => this.render())
} // 初始化
static init() {
window.Router = new RouterClass()
} // 记录 path 对应的 cb 和 cb 的 回调
route(path, cb) {
this.routes[path] = cb || function() {}
} // 入栈当前hash,执行 cb 跳转页面
render() {
if(this.isBack) { // 如果是由backoff进入,则置false之后return
this.isBack = false
return
}
this.currentUrl = location.hash.slice(1) || '/'
this.historyStack.push(this.currentUrl)
this.routes[this.currentUrl]()
} // 路由后退
back() {
this.isback = true
this.historyStack.pop() // 移除当前 hash, 回退到上一个
const { length } = this.historyStack
if(!length) return
let prev = this.historyStack[length -1] // 拿到要回退到的目标hash
location.hash = `#${ prev }`
this.currentStack = prev
this.routes[prev]() // 执行对应cb
}
}
RouterClass.init()
const BtnDom = document.querySelector('button')
const ContentDom = document.querySelector('.content-div')
const changeContent = content => ContentDom.innerHTML = content Router.route('/', () => changeContent('默认页面'))
Router.route('/page1', () => changeContent('page1页面'))
Router.route('/page2', () => changeContent('page2页面')) // bind() 可以改变函数内部的this的指向,并返回一个新的函数,你必须调用它才会被执行
BtnDom.addEventListener('click', Router.back.bind(Router), false)
</script>
</body> </html>
2. HTML5 History Api
2.1 相关 Api
window.history.back(); // 这和用户点击浏览器回退按钮的效果相同
window.history.forward(); // 向前跳转
window.history.go(n); // 跳转到 history 中指定的一个点
window.history.go(-1); // 向后移动一个页面 (等同于调用 back())
window.history.go(1); // 向前移动一个页面, 等同于调用了 forward()
window.history.length; // 长度属性的值来确定的历史堆栈中页面的数量
添加和修改历史记录中的条目
history.pushState() // 追加一条新的历史记录
// history.pushState('状态对象:历史记录的标题', '标题:历史记录的描述', url)
history.replaceState() // 替换当前的历史记录为一条新的记录
// history.replaceState('历史记录的标题', '历史记录的描述', url)
// window.onpopstate 事件 历史切换事件
HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate
配合使用。
pushState() 方法的例子
假设在http://mozilla.org/foo.html
中执行了以下js代码:
let stateObj = {
foo: "bar",
}; history.pushState(stateObj, "page 2", "bar.html");
这将使浏览器地址栏显示为`http://mozilla.org/bar.html
,但是并不会导致浏览器加载bar.html
,甚至不会检查bar.html
是否存在。
假设用户又访问了http://google.com
,然后点击了返回按钮,此时地址栏现实的是`http://mozilla.org/bar.html
,history.state
中包含了stateObj
的一份拷贝。页面此时展现为bar.html
。切页面被重新加载了,所以popstate
事件将不会被触发。
如果我们再次点击返回按钮,页面URL会变为http://mozilla.org/foo.html
,文档对象document会触发另外一个 popstate
事件,这一次的事件对象state object为null。 这里也一样,返回并不改变文档的内容,尽管文档在接收 popstate
事件时可能会改变自己的内容,其内容仍与之前的展现一致。
replaceState() 方法的例子
history.replaceState()
的使用与 history.pushState()
非常相似,区别在于 replaceState()
是修改了当前的历史记录项而不是新建一个。 注意这并不会阻止其在全局浏览器历史记录中创建一个新的历史记录项。
假设http://mozilla.org/foo.html
执行了如下JavaScript代码:
let stateObj = {
foo: "bar",
}; history.pushState(stateObj, "page 2", "bar.html");
然后,假设http://mozilla.org/bar.html
执行了如下 JavaScript:
history.replaceState(stateObj, "page 3", "bar2.html");
这将会导致地址栏显示http://mozilla.org/bar2.html
,但是浏览器并不会去加载bar2.html
甚至都不需要检查 bar2.html
是否存在。
假设现在用户重新导向到了http://www.microsoft.com
,然后点击了回退按钮。这里,地址栏会显示http://mozilla.org/bar2.html
。假如用户再次点击回退按钮,地址栏会显示http://mozilla.org/foo.html
,完全跳过了bar.html。
popstate 事件
每当活动的历史记录项发生变化时, popstate
事件都会被传递给window对象。如果当前活动的历史记录项是被 pushState
创建的,或者是由 replaceState
改变的,那么 popstate
事件的状态属性 state
会包含一个当前历史记录状态对象的拷贝。
使用示例请参见 window.onpopstate
。
获取当前状态
页面加载时,或许会有个非null的状态对象。这是有可能发生的,举个例子,假如页面(通过pushState()
或 replaceState()
方法)设置了状态对象而后用户重启了浏览器。那么当页面重新加载时,页面会接收一个onload事件,但没有 popstate 事件。然而,假如你读取了history.state属性,你将会得到如同popstate 被触发时能得到的状态对象。
你可以读取当前历史记录项的状态对象state,而不必等待popstate
事件, 只需要这样使用history.state
属性:
let currentState = history.state;
2.2 实例
将之前的例子改造一下,在需要路由跳转的地方使用 history.pushState
来入栈并记录 cb
,
前进后退的时候监听 popstate
事件拿到之前传给 pushState
的参数并执行对应 cb
,因为借用了浏览器自己的 Api。
<html lang="en"> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>h5 router</title>
</head> <body>
<ul>
<li><a href="/">/</a></li>
<li><a href="/page1">page1</a></li>
<li><a href="/page2">page2</a></li>
</ul>
<div class='content-div'></div>
<script>
class RouterClass {
constructor(path) {
this.routes = {} // 记录路径标识符对应的cb
history.replaceState({ path }, null, path)
this.routes[path] && this.routes[path]()
window.addEventListener('popstate', e => {
console.log(e, ' --- e')
const path = e.state && e.state.path
this.routes[path] && this.routes[path]()
})
} /**
* 初始化
*/
static init() {
window.Router = new RouterClass(location.pathname)
} /**
* 记录 path 对应 cb
* @param path 路径
* @param cb 回调
*/
route(path, cb) {
this.routes[path] = cb || function() {}
} /**
* 触发路由对应回调
* @param path
*/
go(path) {
history.pushState({
path
}, null, path)
this.routes[path] && this.routes[path]()
}
} RouterClass.init()
const ul = document.querySelector('ul')
const ContentDom = document.querySelector('.content-div')
const changeContent = content => ContentDom.innerHTML = content Router.route('/', () => changeContent('默认页面'))
Router.route('/page1', () => changeContent('page1页面'))
Router.route('/page2', () => changeContent('page2页面')) ul.addEventListener('click', e => {
if (e.target.tagName === 'A') {
e.preventDefault()
Router.go(e.target.getAttribute('href'))
}
})
</script>
</body> </html>
[VUE HTML5 History 模式](https://router.vuejs.org/zh/guide/essentials/history-mode.html#html5-history-%E6%A8%A1%E5%BC%8F)