最近开发的埋点项目,须要记录用户行为轨迹即用户页面访问顺序。须要在页面跳转的时候,记录用户访问的信息(好比 url ,请求头部等),非单页面应用能够给 window 对象加上一个 beforeunload 事件,在页面离开时触发采集开关,可是如今不少业务是单页面应用,用户切换地址的时候,是无刷新的局部更新,没有办法触发 beforeunload。因此单页面应用的路由插件必定运用了 window 自带的,无刷新修改用户浏览记录的方法,pushState 和 replaceState。javascript
history 提供了两个方法,可以无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录html
history 对象的详细信息已经有不少很好很详细的介绍文献,这里再也不作总结,咱们引用阮老师的教程介绍,history对象 -- JavaScript 标准参考教程(alpha)前端
history.pushState方法接受三个参数,依次为:vue
state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。若是不须要这个对象,此处能够填null。java
title:新页面的标题,可是全部浏览器目前都忽略这个值,所以这里能够填null。git
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。 假定当前网址是example.com/1.html,咱们使用pushState方法在浏览记录(history对象)中添加一个新记录。github
var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
复制代码
添加上面这个新记录后,浏览器地址栏马上显示 example.com/2.html,但并不会跳转到 2.html,甚至也不会检查2.html 是否存在,它只是成为浏览历史中的最新记录。这时,你在地址栏输入一个新的地址(好比访问 google.com ),而后点击了倒退按钮,页面的 URL 将显示 2.html;你再点击一次倒退按钮,URL 将显示 1.html。vue-router
总之,pushState 方法不会触发页面刷新,只是致使 history 对象发生变化,地址栏会有反应。跨域
若是 pushState 的 url参数,设置了一个新的锚点值(即hash),并不会触发 hashchange 事件。若是设置了一个跨域网址,则会报错。浏览器
// 报错
history.pushState(null, null, 'https://twitter.com/hello');
上面代码中,pushState想要插入一个跨域的网址,致使报错。这样设计的目的是,防止恶意代码让用户觉得他们是在另外一个网站上。
复制代码
history.replaceState 方法的参数与 pushState 方法如出一辙,区别是它修改浏览历史中当前纪录,假定当前网页是 example.com/example.html。
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back()
// url显示为http://example.com/example.html?page=1
history.back()
// url显示为http://example.com/example.html
history.go(2)
// url显示为http://example.com/example.html?page=3
复制代码
开发过单页面应用的同窗,必定比较清楚,单页面应用的路由切换是无感知的,不会从新进行 http 请求去获取页面,而是经过改变页面渲染视图来实现。因此他的实现原理必定也是经过原生的 pushState 或则 replaceState 来实现的。因此在页面跳转的时候必定会调用 pushState 或则 replaceState ,要记录用户的跳转信息,咱们只要拦截 pushState 和 replaceState,在执行默行为前先执行咱们的方法就可以采集到用户的跳转信息了
// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入咱们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法
collect = {}
collect.onPushStateCallback : function(){} // 自定义的采集方法
(function(history){
var replaceState = history.replaceState; // 存储原生 replaceState
history.replaceState = function(state, param) { // 改写 replaceState
var url = arguments[2];
if (typeof collect.onPushStateCallback == "function") {
collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
}
return replaceState.apply(history, arguments); // 调用原生的 replaceState
};
})(window.history);
复制代码
既然知道了这个原理,咱们来看下 vue-router 的实现,咱们打开 vue-router 项目地址,把项目克隆下来,或则直接在 github 上预览,在 Vue 开发的项目里,咱们经过 router.push('home') 来实现页面的跳转,因此咱们检索下,push 方法的实现
咱们检索到了 20 个 js 文件,😂,通常到这个时候,咱们会放弃源码阅读,那么咱们今天的文章就到这结束,谢谢你们!
开个玩笑,源码阅读不能这么粗糙,咱们找到 src 目录,点开 index.js 文件,看到 history对象的定义和 mode 参数有关
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
复制代码
看到 history 对象的实例与配置的 mode 有关,vue-router 经过3中方式实现了路由切换。与咱们今天讲的内容相匹配的是 HTML5History 的实现方案,其余的将再也不文章中作扩展,若果你感兴趣想要了解,能够看文章后面的扩展阅读
咱们来看 vue-router 中的 HTML5History 源码:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
复制代码
在使用 Vue 开发的过程当中,咱们必定用到过 push 和 replace 来改变路由,和视图。
router 实例调用的 push 实际是 history 的方法,经过 mode 来肯定匹配 history 的实现方案,从代码中咱们看到,push 调用了 src/util/push-state.js 中被改写过的 pushState 的方法,改写过的方法会根据传入的参数 replace?: boolean 来进行判断调用 pushState 仍是 replaceState ,同时作了错误捕获,若是,history 无刷新修改访问路径失败,则调用 window.location.replace(url)
,有刷新的切换用户访问地址 ,同理 pushState 也是这样。这里的 transitionTo 方法主要的做用是作视图的跟新及路由跳转监测,若是 url 没有变化(访问地址切换失败的状况),在 transitionTo 方法内部还会调用一个 ensureURL 方法,来修改 url。 transitionTo 方法中应用的父方法比较多,这里不作长篇赘述,具体代码分析能够关注后我之后的文章
经过上面的学习,咱们知道了,单页面应用路由的实现原理,咱们也尝试去实现一个。在作管理系统的时候,咱们一般会在页面的左侧放置一个固定的导航 sidebar,页面的右侧放与之匹配的内容 main 。点击导航时,咱们只但愿内容进行更新,若是刷新了整个页面,到时导航和通用的头部底部也进行重绘重排的话,十分浪费资源,体验也会很差。这个时候,咱们就能用到咱们今天学习到的内容,经过使用 HTML5 的 pushState 方法和 replaceState 方法来实现,
思路:首先绑定 click 事件。当用户点击一个连接时,经过 preventDefault 函数防止默认的行为(页面跳转),同时读取连接的地址(若是有 jQuery,能够写成.get方法)这个地址中真正的内容,同时替换当前网页的内容。
为了处理用户前进、后退,咱们监听 popstate 事件。当用户点击前进或后退按钮时,浏览器地址自动被转换成相应的地址,同时popstate事件发生。在事件处理函数中,咱们根据当前的地址抓取相应的内容,而后利用 AJAX 拉取这个地址的真正内容,呈现,便可。
最后,整个过程是不会改变页面标题的,能够经过直接对 document.title 赋值来更改页面标题。
好了,咱们今天经过多个方面来说了 pushState 方法和 replaceState 的应用,你应该对这个两个方法能有一个比较深入的印象,若是想要了解更多,你能够参考如下连接