最近做了一个移动端hybrid项目,将H5应用嵌入到app中。
简单介绍:大部分逻辑实现都靠H5实现,包括实现后退功能。一个app内只有一个页面,页面主体分成上下两部分:header和content。所有页面"跳转"都通过js函数对这两部分进行替换实现,每个跳转都有唯一函数。app原声部分有后退键退出app功能,当H5部分无法继续后退(处于history中第一个或者说在history序号为0)时双击可退出应用。
在开发过程早期,我完全使用h5 的history api 进行后退操作。每次js实现"跳转"前,先使用pushState推入历史,state的结构相对简单:
{func:'xx',param:['x','x']}
再设置历史事件函数:
window.onpopstate=function(event){
if(event.state&&event.state.func&&window[event.state.func]&&typeof(window[event.state.func])=='function'){
window[event.state.func].apply({},param);
}
}
每个"跳转"动作通过直接调用各自的js函数来实现,在页面上点击元素"跳转"前,将要调用的js函数名称和参数放入state,然后推入历史。需要返回时直接使用history.back()或者history.go()来实现效果。
后来发现这样实现的限制太大,有以下几点:
1、在使用api的时候只能向前或向后跳跃指定的步数,不能指定跳跃到第index个历史。
2、不知道当前历史在history list中的序号,也无法向前或向后检索到history list 中的某个满足特定条件的历史。
3、在应用中点击链接跳转到其他网站,返回后页面展示应用中主页的内容,但是当前历史很可能不在第一条,这样需要后退多次(每次都是主页内容)才能走到最早的历史,最后才能退出。
4、无法修改历史中state的数据,其中保存的函数参数如果已经过时(发生改变),我无法在跳转之前先修改这部分内容,这样的跳转功能不灵活。
5、只能获取history的总长度,这个数据对判断当前历史的序号一点帮助都没有,甚至想象不到这个数据的任何用处。
想骂一下H5这个history api的功能,真的比较残缺。但是功能全比不全好,有比没有好,有就不错了,要啥自行车。
想解决这些问题,好像没有办法了。
真的没有办法了吗?有时候事情看起来没法做,实际上做到的还是有不少的。
仔细想想,这个api里面我实际需要的东西只有一个,就是页面后退的回调事件,onpopstate。
浏览器把history 的list藏得这么深,老子不用你这个list了,自己维护一个list。
只需要在每次需要的时候能触发onpopstate事件,剩下的判断可以自己实现。
我可以每次都通过window.history.back()来触发onpopstate,在onpopstate中pushState就能让history恢复之前的序号。这样就能一直触发onpopstate。
所有js端调用history的api,创建一个对象来代理history,叫myHistory。
myHistory结构如下:
{list:[],
currentIndex:0,//当前历史序号
ongoingFlag:false,//代理history操作标志
pushState:function(){
},replaceState:function(){
},go:function(){
},back:function(){
}popstate:function(){//在onpopstate中判断如果是myHistory触发事件,调用此方法实现"跳转"效果
var state=this.list[this.currentIndex].state;
var func=state.func;
var param=state.param;
if(func&&window[func]&&'function'==typeof(window[func])){
try{
window[func].apply({},param);
}catch(e){
}
}
}
}
在代理的back、go中做三件事:
1、设置ongoingFlag=true,表示当前myHistory正在使用history api,用来和前进后退键进行区分。
2、重新定位myHistory 列表的当前位置。
3、window.history.back(),这会触发onpopstate。
在代理的pushState中做两件事:
1、判断当前历史在myHistory列表中的序号是否为0,如果是,执行history.pushState(),此时前进键灰化。
2、在myHistory列表的当前项后插入新state对象,并抛弃后面的所有state。
在onpopstate中先进行判断,是代理myHistory操作还是前进后退键操作(上面的ongoingFlag):
1、如果ongoingFlag=false,是前进后退键,全部当作后退处理(前进和后退无法分辨,么有办法)。
1)myHistory当前历史向前跳一步(currentIndex--;popstate();)。
2)判断myHistory当前历史序号是否为0,如果不是,说明未返回到主页,应该执行history.pushState方法,给下次后退操作留下历史。
2、如果ongongFlag=true,是代理myHistory操作,当前历史已经重新定位,只需直接触发muHistory.popstate方法
1)标志复位ongongFlag=false;
2)直接调用myHistory.popstate();
3)同1-2)。
这样改造后的效果是,我可以使用代理的历史对象来很方便地维护自己的历史,可以进行历史的修改、历史的特定条件搜索和指定序号的历史跳转。
这样修改后留下一个前进键的问题,虽然每次跳转后在onpopstate中基本都会发生pushstate的动作,但是后退到主页时是不能pushstate的,否则后退键需要多摁一次才能触发app的退出功能。此时前进键是不会灰化的,这时点击前进键,我们是当作后退来处理,这是个逻辑上的错误,然而不能直接解决。
function historyBack(){
window.stopForwFlag=true;
window.history.back();
}
解决这个问题也是有办法的,如果在onpopstate中history.pushState时推入自定义后退函数historyBack,并且在onpopstate函数前部进行state判断和执行,可以在点击前进键后触发自定义后退函数,执行history.back(),为了防止之后继续触发onpopstate中的逻辑,在historyBack函数中设置stopForwFlag=true,同时onpopsate函数最前部判断此值如果为true,设置成false后立即返回。
最后应用中在非主页时前进键灰化,在主页时前进键点击后立即后退,实现了一个不太完美但是更为灵活的历史api。