本文介绍如何使用backbone的history模块实现SPA应用里面的URL管理。SPA应用的核心在于使用无刷新的方式更改url,从而引发页面内容的改变。从实现上来看,url的管理和页面内容的管理是其中的两个难点。就url的管理而言,主要有以下三方面的要求:
1)对于要采用单页跳转的链接,不能有页面刷新;
2)浏览器的前进和后退,都能像多页应用那样,显示之前访问地址对应的内容;
3)应用处于任何一个单页链接地址时,当用户刷新,依然能初始化显示该地址对应的内容。
假如要自己来实现一个能够满足以上三方面要求的功能,思路有2种。
第一种是利用锚点链接及hashchange,将所有单页链接地址全部配置成锚点链接的形式,然后在hashchange事件里面,根据页面当前的锚点值,执行不同的回调函数,用于更改页面内容。这个思路在上一篇博客中《理解浏览器历史记录(2)-hashchange、pushState》给出了一个简单的实现(demo),代码虽然比较简陋,但是也说明思路是可行的。
第二种是利用pushState,详细步骤如下:
1)在点击链接的时候,如果这个链接是单页形式的链接,可通过pushState或者replaceState的方式来改变url;由于pushState跟replaceState并不会触发popstate事件,所以在必要的条件下,还得在调用完pushState和replaceState调用完后,手动调用相应的回调函数;
2)监听浏览器的popstate事件,这样就能响应浏览器前进后退的操作,然后根据操作后的页面地址找到对应的回调函数执行;
3)页面初始化时,直接根据当前地址执行对应的回调函数即可。
上次也没有给出简单使用pushState实现SPA的例子,这次补上,功能与hashchange那个类似,就是写法稍微有点不同而已。demo地址(不可测刷新操作,如果使用pushState,单页地址刷新会报404,需在后台处理才能解决,此处毕竟只是个静态页):
http://liuyunzhuge.github.io/blog/pushState/demo7.html
比较起来,用hashchange做spa的方法要简单一些,而且兼容性更好,加上mdn上给出的用定时器来模拟hashchange的方法,基本上用它是可以实现全浏览器兼容的SPA应用了。不过hashchange相比pushstate也有一些大的弊端:
一是url对自己不够友好,本来在多页应用里面,url可能都是绝对路径或者相对路径开头的形式,如果全换成#hash的形式,显然违背自己多年的使用习惯;
二是url对搜索引擎不够友好。
所以要是能够把hashchange,pushstate结合起来实现SPA里面的url管理,显然这个事情就变得很完美了。事实上已经有很多的所谓的路由框架做到这一点,我也没有去研究别的,加上一直对backbone这个框架的评价不错,所以就琢磨着怎么用它实现我所需要的SPA的url管理了。
下面的内容就是介绍如何使用backbone的history模块来实现spa的url管理。由于只是介绍方法,所以对相关的api的使用不会一一进行说明。感兴趣地可以查阅backbone的官方文档,主要是Router和History模块的部分。要是想详细了解浏览器如何管理历史记录,以及hashchange和pushstate使用要点的话,也可以看看我前面两篇博客写的一些基础知识总结,它们对于我理解Backbone的history模块还是很有帮助。
理解浏览器历史记录(2)-hashchange、pushState;
理解浏览器的历史记录;
history使用介绍
按照官方文档的说明,backbone的history模块可以实现从pushstate到hashchange到url直接跳转,这三种方式的优雅降级。为了验证这一点,我专门做了几个demo。
先来看demo1:http://liuyunzhuge.github.io/blog/backbone_router/demo1.html
在正式介绍这个demo前,先要了解一点,网页里的a标签的href属性有多种形式:
1)http(s)形式的,如href="/index",href="../index",href="http://www.baidu.com",href="https://www.baidu.com"
2)锚点形式,如href="#container"
3)脚本形式,如href="javascript:;"
4)拨打电话,如href="tel:95555"
5)邮件链接,如href="mailto:xxx@yyy.com"
6)其它自定义的形式等
a标签被点击时,浏览器都会有默认的行为,比如http(s)形式的链接会引发网页的跳转;锚点形式的链接会引发hashchange以及锚点内容的定位。在单页应用里面,大部分的http(s)形式的链接都要采用单页形式跳转,但也不是所有的链接都要使用单页形式跳转。在管理url的时候,要区分出哪些链接需要用单页形式的跳转。backbone的history模块可以解决的是url管理问题,但是不会解决如何区分哪些链接需要单页形式跳转的问题。当然实际上解决后面这个问题也很简单,后面的部分会详细说明,这里只是说明这个实现的要点。
接下来在chrome中新开一个选项卡,打开上面的demo1链接以及控制台,可看到下面类似的界面:
做一些测试:点击列表链接;点击详情链接;点击关于链接;点击浏览器倒退;点击浏览器倒退;点击浏览器倒退;点击浏览器前进;点击浏览器前进;点击浏览器前进。观察浏览器地址栏以及控制台的变化(由于没有考虑在url更改的时候去更改页面内容,所以只能用控制台的打印的方式来简单代替一下url变化的页面内容更改的逻辑)
比如点击列表链接后,页面状态应该是这样的:
在所有的操作中,不难发现整个页面都是满足SPA要求的(当然,这个demo不能做刷新操作测试,道理同上)。接下来看看这个demo的一些要点:
首先为了解决前面的提出的区分链接是否要做单页跳转的问题,在需要单页跳转的a元素上增加了一个spa的属性:
<li><a spa href="/">首页</a></li>
<li><a spa href="/list">列表</a></li>
<li><a spa href="/detail/1">详情</a></li>
<li><a spa href="/about">关于</a></li>
demo里面除了顶部的四个链接有加spa的标识,还给出了一些普通链接,点击之后会按照默认行为进行交互,用来对比单页链接的行为。观察这个html结构,也不难发现,尽管这些链接都是单页的,但是它们的href的形式还跟多页应用里的书写方式一样。
然后考虑如何自定义这些单页链接的行为。我的实现是通过事件委托的方式,在document对象上代理这些a元素的点击事件:
$(document).on('click', 'a[spa]', function (e) {
var $a = $(this);
var href = $.trim($a.attr('href')); var options = {
trigger: $a.data('trigger') !== false,//默认点击链接后都要自动触发回调
replace: Backbone.history.getFragment(href) == Backbone.history.fragment//如果链接地址与当前地址匹配就采用replace的方式
}; Backbone.history.navigate(href, options); e.preventDefault();
});
然后在这个代码的内部,取消浏览器的默认行为,然后很简单的调用Backbone.history.navigate方法来完成url的跳转,它会根据浏览器对pushState的支持情况以及history模块初始化的方式,来判断是用pushstate api更改url还是通过hash来更改url。这个方法的第二参数,是一个选项配置对象,trigger属性决定是否在url跳转完毕后,自动触发相应的回调函数;replace属性,决定了是否在浏览器历史记录栈中创建新的条目。考虑到这里是全局定义单页链接的行为,replace属性是否为true完全根据链接的地址是否与当前url匹配决定的,如果匹配那么replace就是true,意味着链接是重复点击,不需要创建新的历史记录条目,否则就是false,意味着当前链接点击后需要跳转到一个新的地址,所以需要创建新的历史记录条目;trigger属性,除非显示地在a元素上加了data-trigger=false,否则它传递到navigate始终是true。
trigger属性设置为true是比较关键的,尤其是当pushstate被使用的时候,navigate内部仅仅只是用pushstate api改变了页面地址,所以要手工地去触发相应的回调函数执行。
接着在demo里面还有下面一段简单的代码,用到了Backbone.Router模块,用来完成url规则与回调函数的定义:
var AppRouter = Backbone.Router.extend({
routes: {
'': 'index',
'index': 'index',
'list': 'renderList',
'detail/:id': 'renderDetail',
'*error': 'renderError'
},
index: function () {
console.log('首页action');
},
renderList: function () {
console.log('列表action');
},
renderDetail: function (id) {
console.log('详情action, 详情id为: ' + id);
},
renderError: function (error) {
console.log('URL错误, 错误信息: ' + error);
}
}); var router = new AppRouter();
Router模块是一个很简单的模块,它就是一个url规则与回调函数的映射表,被History模块的实例所引用,当页面地址匹配到了Router里面定义的规则时,相应的回调函数就会执行。上面的用法已经比较全了,实际项目中,可以有多个AppRouter这样的模块定义,然后各自初始化一个Router实例,这样就能实现大量的url规则进行拆分管理。
最后通过下面的方式,来初始化启用了pushstate管理的history模块:
Backbone.history.start({pushState: true, root: ROOT_BASE + '/demo1.html/'});
(root的作用可以查看backbone官方文档了解)
以上就是本文要介绍的最重要的内容,它是一种比较简洁的进行spa的url管理的方法,尽管可能还有一些场景没有考虑到,但是按照以上思路,即使碰到一些使用不佳的场景,我们也能够想办法解决。
回到本部分开头的内容,来看看backbone是否真能做到url管理的优雅降级。为了验证这一点,需要模拟另外两种场景,分别是不支持pushState的场景和不使用pushState&hashchange的场景。
demo2:http://liuyunzhuge.github.io/blog/backbone_router/demo2.html
demo2与demo1的区别仅仅在于最后面初始化history模块的方式,demo1是这样的:
Backbone.history.start({pushState: true, root: ROOT_BASE + '/demo1.html/'});
demo2是这样的:
Backbone.history.start({root: ROOT_BASE + '/demo2.html/'});
demo2的初始化代码,没有传递pushState属性,在backbone内部会被认为不需要使用pushstate,现在电脑上安的浏览器基本上都支持pushstate,所以在不更改backbone源码的前提下,只能采取这种形式来模拟一种pushstate不支持,降级到hashchange处理的场景。
好在这个demo里面,其它的代码形式都没有改变,最主要是单页链接的地址还是原来的那个形式:
<li><a spa href="/">首页</a></li>
<li><a spa href="/list">列表</a></li>
<li><a spa href="/detail/1">详情</a></li>
<li><a spa href="/about">关于</a></li>
接下来在chrome中新开一个选项卡,打开demo2的链接,按照demo1的测试操作,做一下测试:点击列表链接;点击详情链接;点击关于链接;点击浏览器倒退;点击浏览器倒退;点击浏览器倒退;点击浏览器前进;点击浏览器前进;点击浏览器前进。观察地址栏及控制台的变化。
最后会发现,控制台的打印情况跟demo1是一致的。但是浏览器的地址不再是demo1那种形式的地址,比如点击列表链接后,页面地址变成了这种hash形式:
说明这个场景下的navigate方法,其实是通过改变hash来实现的。然后浏览器前进后退的时候也应该是通过hashchange事件来实现的。最后这个demo可以证明,backbone的history模块,确实可以完美地降级到hashchange来管理url,而且这个变化对于我们的业务代码来说是透明的。
demo3:http://liuyunzhuge.github.io/blog/backbone_router/demo3.html
这个demo用来模拟连hashchange都无法使用的场景。它与demo1的区别也仅仅在于history模块初始化的方式,它的是:
Backbone.history.start({hashChange: false, root: ROOT_BASE + '/demo3.html/'});
通过显示的配置hashChange为false,告诉backbone不使用hashchange管理url。在这个场景下backbone.history.navigate方法,会直接进行url跳转,而不会用到pushstate以及hash。正是如此,navigate最后并不会触发url的回调函数,毕竟页面已经重载了。但是backbone能做到的就是,当页面初始化的时候,与页面地址匹配的回调函数还是会被执行,这也就保证了它在这种场景下,url地址与页面内容的一致性。
不过这个demo3打开后,点击那些“单页链接”,最后都会报404,毕竟它只是用来模拟这个特殊情况而已。但是从它start方法的源码来看,backbone确实能做到在页面初始化的时候执行与它对应的回调。通过这个demo,最后完全证明了backbone在url管理时的兼容程度,确实很全面。实际工作中,可以考虑仅借鉴demo1的方法即可。
最后,还要再补充一下的就是如果是通过demo1那种方式初始化History模块,那么demo3这个场景应该在绝大部分浏览器都不会出现。因为即使在那些不支持hashchange的浏览器上,backbone内部也通过定时器加iframe的方式,来模拟hashchange。也就是说如果不手动把hashchange配置为false的话,backbone总会有一个模拟的hashchange作为备用。这样它就能让你的应用始终保持SPA的状态了。要想了解backbone内部管理url的细节,可以仅看backbone源码部分的router模块和history模块,最主要的就是history模块的start和navigate方法,这两个方法是url管理初始化和跳转控制的核心,从中也能学到一些关于location使用的技巧。
小结
这篇文章的目的就是为了介绍spa里面url管理的思路以及backbone来管理url的做法。由于我到目前为止,还没做过单页的应用,所以这里面有的方法或者观点不一定完全正确,希望有经验的朋友看到之后能帮忙指正,感谢~下一篇介绍如何管理SPA里面的页面内容,方法正在琢磨中…