单页面路由实现原理

1,091 阅读7分钟

这是我参与更文挑战的第13天,活动详情查看: 更文挑战

由于项目马上上线了,之前并没有考虑到用户行为轨迹的分析。最近刚好准备更新下前端的性能监控平台,正好做一下,顺便系统分析一下前端路由的实践。记得两年前做android开发的时候,客户oem版的让接入google分析来做页面流的分析与错误监控,当时是埋点做的,前端页面流最好还是通过路由来做,这样埋点位置更好控制点。

后端路由简介

路由这个概念最先是后端出现的。在以前用模板引擎开发页面时,经常会看到这样

http://www.xxx.com/login

大致流程可以看成这样:

  • 浏览器发出请求
  • 服务器监听到80端口(或443)有请求过来,并解析url路径
  • 根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等)
  • 浏览器根据数据包的 Content-Type 来决定如何解析数据

简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。

单页面切换

用户切换地址的时候,是无刷新的局部更新,没有办法触发 beforeunload。所以单页面应用的路由插件一定运用了 window 自带的,无刷新修改用户浏览记录的方法,pushState 和 replaceState。而用户的再非单页面的应用可以使用window.beforeunload事件,页面离开时触发。

history对象的几个方法

history 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录 。

history.pushState

history.pushState方法接受三个参数,依次为:

  • state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
  • title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。 假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

添加上面这个新记录后,浏览器地址栏立刻显示 example.com/2.html,但并不会跳转到 2.html,甚至也不会检查2.html 是否存在,它只是成为浏览历史中的最新记录。这时,你在地址栏输入一个新的地址(比如访问 google.com ),然后点击了倒退按钮,页面的 URL 将显示 2.html;你再点击一次倒退按钮,URL 将显示 1.html。

总之,pushState 方法不会触发页面刷新,只是导致 history 对象发生变化,地址栏会有反应。 如果 pushState 的 url参数,设置了一个新的锚点值(即hash),并不会触发 hashchange 事件。如果设置了一个跨域网址,则会报错。

// 报错
history.pushState(null, null, 'https://twitter.com/hello');

上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上。

history.replaceState

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

history.back、history.forward、history.go,length

在HTML4,Histroy对象有下面属性方法:

  • History.back():移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果。
  • History.forward():移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果。
  • History.go():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址,比如go(1)相当于forward(),go(-1)相当于back()。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为0,相当于刷新当前页面。

history.go(0)相当于刷新当前页面。

移动到以前访问过的页面时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。

popstate 事件

每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。

注意,仅仅调用pushState()方法或replaceState()方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()、History.forward()、History.go()方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。

使用的时候,可以为popstate事件指定回调函数。

window.onpopstate = function (event) {
  console.log('location: ' + document.location);
  console.log('state: ' + JSON.stringify(event.state));
};

// 或者
window.addEventListener('popstate', function(event) {
  console.log('location: ' + document.location);
  console.log('state: ' + JSON.stringify(event.state));
});

回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前 URL 所提供的状态对象(即这两个方法的第一个参数)。上面代码中的event.state,就是通过pushState和replaceState方法,为当前 URL 绑定的state对象。

这个state对象也可以直接通过history对象读取。

var currentState = history.state;

注意,页面第一次加载的时候,浏览器不会触发popstate事件。

模拟单页面路由

在做管理系统的时候,我们通常会在页面的左侧放置一个固定的导航 sidebar,页面的右侧放与之匹配的内容 main 。点击导航时,我们只希望内容进行更新,如果刷新了整个页面,到时导航和通用的头部底部也进行重绘重排的话,十分浪费资源,体验也会不好。这个时候,我们就能用到我们今天学习到的内容,通过使用 HTML5 的 pushState 方法和 replaceState 方法来实现,

  • 首先绑定 click 事件。当用户点击一个链接时,通过 preventDefault 函数防止默认的行为(页面跳转),同时读取链接的地址(如果有 jQuery,可以写成(this).attr('href')),把这个地址通过pushState塞入浏览器历史记录中,在利用ajax拉取(可以使用jquery的get()方法)这个地址中的真正内容,同时替换当前网页的内容。

为了处理用户前进、后退,我们监听 popstate 事件。当用户点击前进或后退按钮时,浏览器地址自动被转换成相应的地址,同时popstate事件发生。在事件处理函数中,我们根据当前的地址抓取相应的内容,然后利用 AJAX 拉取这个地址的真正内容,呈现,即可。 最后,整个过程是不会改变页面标题的,可以通过直接对 document.title 赋值来更改页面标题。

单页面应用用户访问轨迹埋点

开发过单页面应用的同学,一定比较清楚,单页面应用的路由切换是无感知的,不会重新进行 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);

总结

本文通过多个方面来讲了 pushState 方法和 replaceState 的应用

以上! 最后的惯例,贴上我的博客,欢迎关注

请关注公众号:全栈飞行中队