标准定义
关于历史的管理,和HTML页面加载一样,都有其相应的标准。地址如下:
WhatWG: https://html.spec.whatwg.org/multipage/browsers.html#history
其中关于历史项的要点如下:
1. 在onload之前,非用户操作引起的导航操作不建立历史项。
非用户操作比如页面中指定的Timer修改location或iframe的src引发的导航操作。而用户点击发起的Timer,则在timer中记录下在发起timer时标记手势来源。但有一个例外是由另一个Timer发起的Timer或是重复执行的Timer, 则仅针对第一次执行有效(以nesting level标识)。
2. 子Frame加载完成仍有未加载的上层Frame, 则不创建历史项。
3. 当前Frame只有一个历史项,且为about:blank, 则不创建历史项。
4. 如果页面跳转的间隔小于1s,则不创建历史项。
关于Timer的nesting level可以参考这里:
http://www.w3.org/html/wg/drafts/html/CR/webappapis.html#timer-nesting-level
以上规则对应于以下三个WebKit的函数:
a. LockBackForwardList NavigationScheduler::mustLockBackForwardList(Frame& targetFrame)
LockBackForwardList NavigationScheduler::mustLockBackForwardList(Frame& targetFrame)
{
// Non-user navigation before the page has finished firing onload should not create a new back/forward item.
// See https://webkit.org/b/42861 for the original motivation for this.
if (!ScriptController::processingUserGesture() && targetFrame.loader().documentLoader() && !targetFrame.loader().documentLoader()->wasOnloadHandled())
return LockBackForwardList::Yes;
// Navigation of a subframe during loading of an ancestor frame does not create a new back/forward item.
// The definition of "during load" is any time before all handlers for the load event have been run.
// See https://bugs.webkit.org/show_bug.cgi?id=14957 for the original motivation for this.
for (Frame* ancestor = targetFrame.tree().parent(); ancestor; ancestor = ancestor->tree().parent()) {
Document* document = ancestor->document();
if (!ancestor->loader().isComplete() || (document && document->processingLoadEvent()))
return LockBackForwardList::Yes;
}
return LockBackForwardList::No;
}
b. HistoryController::currentItemShouldBeReplaced() const
{
// From the HTML5 spec for location.assign():
// "If the browsing context‘s session history contains only one Document,
// and that was the about:blank Document created when the browsing context
// was created, then the navigation must be done with replacement enabled."
return m_currentItem && !m_previousItem && equalIgnoringCase(m_currentItem->urlString(), blankURL());
}
c. void NavigationScheduler::scheduleRedirect(double delay, const URL& url)
{
if (!shouldScheduleNavigation(url))
return;
if (delay < 0 || delay > INT_MAX / 1000)
return;
if (url.isEmpty())
return;
// We want a new back/forward list item if the refresh timeout is > 1 second.
if (!m_redirect || delay <= m_redirect->delay()) {
LockBackForwardList lockBackForwardList = delay <= 1 ? LockBackForwardList::Yes : LockBackForwardList::No;
schedule(std::make_unique<ScheduledRedirect>(delay, m_frame.document()->securityOrigin(), url, LockHistory::Yes, lockBackForwardList));
}
}
页面问题分析
1. Timer问题
Timer最大的问题来源于Timer Nesting Level的认定。最初WebKit建一个全局的变量记录, 容易受到不同页面,frame中Timer的影响。
比如用户点击一条链接就使用一个Timer跳转,如果这个Timer创建时,恰好有另一个repeat timer还在循环执行, 且未加载完成(onload事件没有执行), 那它就无法创建历史项,因为这时全局的nesting level大于1,将不会记录为手势触发的跳转。WebKit提交的(https://bugs.webkit.org/show_bug.cgi?id=136401 [^]), 本意用于避免多线程访问的问题,也刚好解决了嵌套层次错误的问题。
现在仍然有一种情况下,这个问题还会存在。见 https://bugs.webkit.org/show_bug.cgi?id=137631。如下面测试页面所示,当在一个document中,有一个repeat timer执行后, 且未加载完成(onload事件没有执行),其它Timer的跳转仍然无法创建历史项。
<html>
<head>
</head>
<body>
<input type="button" value="Goto next page" onclick="gotoNextPage();">
<input type="button" value="Start repeating timer" onclick="startRepeatingTimer();">
<p>
<div id="Timer">Paused</div>
</p>
<script type="text/javascript">
function gotoNextPage(){
setTimeout(function(){location.href="http://www.webkit.org/";},300);
}
function startRepeatingTimer(){
setInterval(function(){document.getElementById("Timer").innerHTML="Running...";},500);
}
</script>
<img src="http://therecoveringpolitician.com/wp-content/uploads/2013/01/moderate.jpg" width="640" height="480">
<img src="http://cullogo.com/full/wallpapers-high-resolution-widescreen-hd-pk.jpg" width="640" height="480">
<img src="http://www.highresolutionwallpapers.net/wallpapers/autumn-1680x1050.jpg" width="640" height="480">
</body>
</html>
2. 加载规则导致多余历史项问题
有点页面(华声论坛)由于页面在iframe的初始化及动态改变时,看起来两个src是相同的,都带有ampersand, 一个由HTML指定,另一个由JavaScript设定。
问题在于JavaScript不会转换ampersand, 这样就相当于iframe加载了两个不同的页面,从而产生了两个历史项。
使用下面的测试页面可以重现这个问题, 注意页面中两处http://m.baidu.com的写法:
<html>
<head>
</head>
<body>
<p>
<div id="info">Loading...</div>
</p>
<script type="text/javascript">
setTimeout(function(){ attr("sub_iframe","http://m.baidu.com/?abc=23&t=20080225");},5000);
function attr(id,url){
var obj=document.getElementById(id);
obj.setAttribute("src", url);
}
window.onload=function(){
document.getElementById("info").innerHTML="Loaded.";
}
</script>
<iframe src="http://m.baidu.com/?abc=23&t=20080225" id="sub_iframe" width="320" height="480"></iframe><br/>
</body>
</html>