博客 有更多精品文章哟。javascript
在深刻了解 History API 以前,咱们须要讨论一下前端路由;路由指的是经过不一样 URL 展现不一样页面或者内容的功能,这个概念最初是由后端提出的,所以,在传统的 Web 开发模式中,路由都是服务器来控制和管理的。css
既然已经有了后端路由,为何还须要前端路由呢?咱们知道跳转页面实际上就是为了展现那个页面的内容,那么不管是选择 AJAX 异步的方式获取数据仍是将页面内容保存在本地,都是为了让页面之间的交互没必要每次都刷新页面,这样用户体验会有极大的提高,也就能被称为 SPA(单页面应用)了;可是,不够完美,由于这种场景下缺乏路由功能,因此会致使用户屡次获取页面以后,不当心刷新当前页面,会直接退回到页面的 初始状态,用户体验极差。html
那么前端路由是怎样解决改变页面内容的同时改变 URL 并保持页面不刷新呢?这就引出了咱们这篇文章的主题:History API。前端
DOM window
对象经过 history
对象提供了对 当前会话(标签页或者 frame
)浏览历史的访问,在 HTML4 的时候咱们已经可以操纵浏览历史向前或向后跳转了;当时,咱们可以使用的属性和方法有下面这些:java
window.history.length
:返回当前会话浏览过的页面数量。window.history.go(?delta)
:接受一个整数做为参数,按照当前页面在会话浏览历史记录中的位置为基准进行移动。若是参数为 0 或 undefined、null、false,将刷新页面,至关于执行 window.location.reload()
。若是在运行这个方法的过程当中,发现移动后会超出会话浏览历史记录的边界时,将没有任何效果,而且也不会报错。window.history.back()
:移动到上一页,至关于点击浏览器的后退按钮,等价于 window.history.go(-1)
。window.history.forward()
:移动到下一页,至关于点击浏览器的前进按钮,等价于 window.history.go(1)
。
window.history.back()
和window.history.forward()
就是经过window.history.go(?delta)
实现的,所以,若是没有上一页或者下一页,那表示会超出边界,因此它们的处理方式和window.history.go(?delta)
是同样的。node
HTML4 的时候并无可以改变 URL 的 API;可是,从 HTML5 开始,History API 新增了操做会话浏览历史记录的功能。如下是新增的属性和方法:react
window.history.state
:这个参数是只读的,表示与会话浏览历史的当前记录相关联的状态对象。window.history.pushState(data, title, ?url)
:在会话浏览历史记录中添加一条记录。如下是方法的参数详情:
data
(状态对象):是一个能被序列化的任何东西,例如 object、array、string、null 等。为了方便用户从新载入时使用,状态对象会在序列化以后保存在本地;此外,序列化以后 的状态对象根据浏览器的不一样有不同的大小限制(注意:规范 并无说须要限制大小),若是超出,将会抛出异常。title
(页面标题):当前全部的浏览器都会忽略这个参数,所以能够置为空字符串。url
(页面地址):若是新的 URL 不是绝对路径,那么将会相对于当前 URL 处理;而且,新的 URL 必须与当前 URL 同源,不然将抛出错误。另外,该参数是可选的,默认为当前页面地址。window.history.replaceState(data, title, ?url)
:与 window.history.pushState(data, title, ?url)
相似,区别在于 replaceState
将修改会话浏览历史的当前记录,而不是新增一条记录;可是,须要注意:调用 replaceState
方法仍是会在 全局 浏览历史记录中建立新记录 。调用 pushState
和 replaceState
方法以后,地址栏会更改 URL,却不会当即加载新的页面,等到用户从新载入时,才会真正进行加载。所以,同源的目的 是为了防止恶意代码让用户觉得本身处于另外一个页面。git
popstate
事件每当用户导航会话浏览历史的记录时,就会触发 popstate
事件;例如,用户点击浏览器的倒退和前进按钮;固然这些操做在 JavaScript 中也有对应的 window.history.back()
、window.history.forward()
和 window.history.go(?delta)
方法可以达到一样的效果。github
若是导航到的记录是由 window.history.pushState(data, title, ?url)
建立或者 window.history.replaceState(data, title, ?url)
修改的,那么 popstate
事件对象的 state
属性将包含导航到的记录的状态对象的一个 拷贝。segmentfault
另外,若是用户在地址栏中 手动 修改 hash
或者经过写入 window.location.hash
的方式来 模拟用户 行为,那么也会触发 popstate
事件,而且还会在会话浏览历史中新增一条记录。须要注意的是,在调用 window.history.pushState(data, title, ?url)
时,若是 url
参数中有 hash
,并不会触发这一条规则;由于咱们要知道,pushState
只是致使会话浏览历史的记录发生变化,让地址栏有所反应,并非 用户导航 或者经过脚原本 模拟用户 的行为。
在介绍 HTML5 中 history
对象新增的属性和方法时,有说道 window.history.state
属性,经过它咱们也能获得 popstate
事件触发时获取的状态对象。
在用户从新载入页面时,popstate
事件并不会触发,所以,想要获取会话浏览历史的当前记录的状态对象,只能经过 window.history.state
属性。
Location 对象提供了 URL 相关的信息和操做方法,经过 document.location
和 window.location
属性都能访问这个对象。
History API 和 Location 对象其实是经过地址栏中的 URL 关联 的,由于 Location 对象的值始终与地址栏中的 URL 保持一致,因此当咱们操做会话浏览历史的记录时,Location 对象也会随之更改;固然,咱们修改 Location 对象,也会触发浏览器执行相应操做而且改变地址栏中的 URL。
Location 对象提供如下属性:
window.location.href
:完整的 URL;http://username:password@www.test.com:8080/test/index.html?id=1&name=test#test
。window.location.protocol
:当前 URL 的协议,包括 :
;http:
。window.location.host
:主机名和端口号,若是端口号是 80
(http)或者 443
(https),那就会省略端口号,所以只会包含主机名;www.test.com:8080
。window.location.hostname
:主机名;www.test.com
。window.location.port
:端口号;8080
。window.location.pathname
:URL 的路径部分,从 /
开始;/test/index.html
。window.location.search
:查询参数,从 ?
开始;?id=1&name=test
。window.location.hash
:片断标识符,从 #
开始;#test
。window.location.username
:域名前的用户名;username
。window.location.password
:域名前的密码;password
。window.location.origin
:只读,包含 URL 的协议、主机名和端口号;http://username:password@www.test.com:8080
。除了 window.location.origin
以外,其余属性都是可读写的;所以,改变属性的值能让页面作出相应变化。例如对 window.location.href
写入新的 URL,浏览器就会当即跳转到相应页面;另外,改变 window.location
也能达到一样的效果。
// window.location = 'https://www.example.com';
window.location.href = 'https://www.example.com';
复制代码
须要注意的是,若是想要在同一标签页下的不一样 frame
(例如父窗口和子窗口)之间 跨域 改写 URL,那么只能经过 window.location.href
属性,其余的属性写入都会抛出跨域错误。
hash
改变 hash
并不会触发页面跳转,由于 hash
连接的是当前页面中的某个片断,因此若是 hash
有变化,那么页面将会滚动到 hash
所连接的位置;固然,页面中若是 不存在 hash
对应的片断,则没有 任何效果。这和 window.history.pushState(data, title, ?url)
方法很是相似,都能在不刷新页面的状况下更改 URL;所以,咱们也可使用 hash
来实现前端路由,可是 hash
相比 pushState
来讲有如下缺点:
hash
只能修改 URL 的片断标识符部分,而且必须从 #
开始;而 pushState
却能修改路径、查询参数和片断标识符;所以,在新增会话浏览历史的记录时,pushState
比起 hash
来讲更符合之前后端路由的访问方式,也更加优雅。
// hash
http://www.example.com/#/example
// pushState
http://www.example.com/example
复制代码
hash
必须与原先的值不一样,才能新增会话浏览历史的记录;而 pushState
却能新增相同 URL 的记录。
hash
想为新增的会话浏览历史记录关联数据,只能经过字符串的形式放入 URL 中;而 pushState
方法却能关联全部能被序列化的数据。
hash
不能修改页面标题,虽然 pushState
如今设置的标题会被浏览器忽略,可是并不表明之后不会支持。
hashchange
事件咱们能够经过 hashchange
事件监听 hash
的变化,这个事件会在用户导航到有 hash
的记录时触发,它的事件对象将包含 hash
改变前的 oldURL
属性和 hash
改变后的 newURL
属性。
另外,hashchange
事件与 popstate
事件同样也不会经过 window.history.pushState(data, title, ?url)
触发。
Location 对象提供如下方法:
window.location.assign(url)
方法接受一个 URL 字符串做为参数,使得浏览器马上跳转到新的 URL。
document.location.assign('http://www.example.com');
// or
// document.location = 'http://www.example.com';
复制代码
window.location.replace(url)
方法与window.location.assign(url)
实现同样的功能,区别在于 replace
方法执行后跳转的 URL 会 覆盖 浏览历史中的当前记录,所以原先的当前记录就在浏览历史中 删除 了。
window.location.reload(boolean)
方法使得浏览器从新加载当前 URL。若是该方法没有接受值或值为 false
,那么就至关于用户点击浏览器的刷新按钮,这将致使浏览器 拉取缓存 中的页面;固然,若是没有缓存,那就会像执行 window.location.reload(true)
同样,从新请求 页面。
window.location.toString()
方法返回整个 URL 字符串。
window.location.toString();
// or
// window.location.href;
复制代码
在使用 History API 实现路由时,咱们要注意这个 API 里的方法(pushState
和 replaceState
)在改变 URL 时,并不会触发事件;所以想要像 hash
同样 只经过 事件(hashchange
)实现路由是不太可能了。
既然如此,咱们就须要知道哪些方式可以触发 URL 的更新了;在单页面应用中,URL 改变只能由下面三种状况引发:
a
标签。pushState
或者 replaceState
方法。对于用户手动点击浏览器的前进或后退按钮的操做,经过监听 popstate
事件,咱们就能知道 URL 是否改变了;点击 a
标签实际上也是调用了 pushState
或者 replaceState
方法,只不过由于 a
标签会有 默认行为,因此须要阻止它,以免进行跳转。
<!-- index.html -->
<!DOCTYPE html>
<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>前端路由实现</title>
<style> .link { color: #00f; cursor: pointer; } .link:hover { text-decoration: underline; } </style>
</head>
<body>
<ul>
<li><a class="link" data-href="/111">111</a></li>
<li><a class="link" data-href="/222">222</a></li>
<li><a class="link" data-href="/333">333</a></li>
</ul>
<div id="content"></div>
<script src="./router.js"></script>
<script> // 建立实例 const router = new Router(); const contentDOM = document.querySelector('#content'); // 注册路由 router.route('/111', state => { contentDOM.innerHTML = '111'; }); router.route('/222', state => { contentDOM.innerHTML = '222'; }); router.route('/333', state => { contentDOM.innerHTML = '333'; }); </script>
</body>
</html>
复制代码
// router.js
const noop = () => undefined;
class Router {
constructor() {
this.init();
}
// 初始化
init() {
this.routes = {};
this.listen();
this.bindLink();
}
// 所有的监听事件
listen() {
window.addEventListener('DOMContentLoaded', this.listenEventInstance.bind(this));
window.addEventListener('popstate', this.listenEventInstance.bind(this));
}
unlisten() {
window.removeEventListener('DOMContentLoaded', this.listenEventInstance);
window.removeEventListener('popstate', this.listenEventInstance);
}
// 监听事件后,触发路由的回调
listenEventInstance() {
this.trigger(this.getCurrentPathname());
};
getCurrentPathname() {
return window.location.pathname;
}
// 注册路由
route(pathname, callback = noop) {
this.routes[pathname] = callback;
}
// 触发回调
trigger(pathname) {
if (!this.routes[pathname]) {
return;
}
const {state} = window.history;
this.routes[pathname](state);
}
// 绑定 a 标签,阻止默认行为
bindLink() {
document.addEventListener('click', e => {
const {target} = e;
const {nodeName, dataset: {href}} = target;
if (!nodeName === 'A' || !href) {
return;
}
e.preventDefault();
window.history.pushState(null, '', href);
this.trigger(href);
});
}
}
复制代码
生成 Router
的实例时,咱们须要作如下工做:
key
是路径名,value
是触发的回调。popstate
和 DOMContentLoaded
事件;在上文咱们已经知道 popstate
事件在页面加载时并不会触发,所以须要监听 DOMContentLoaded
事件来触发初始的 URL 的回调。a
标签,以便咱们在阻止默认行为以后,可以调用 pushState
或 replaceState
方法来更新 URL,并触发回调。注册路由其实上就是在 路由映射对象 中为 路径 绑定 回调,由于 URL
改变后会执行回调,因此咱们能够在回调中改变内容;这样一个很简单的前端路由就实现了。
到此为止,咱们深刻的了解了 History API 和 Location 对象,并理清了它们之间的关系。最重要的是须要明白为何须要前端路由以及适合在什么样的场景下使用;另外,咱们也经过 History API 实现了一个小巧的前端路由,虽然这个实现很简单,可是五脏俱全,经过它能很清晰的知道像 React、Vue 之类的前端框架的路由实现原理。