首页 > 代码库 > WebKit的历史项管理

WebKit的历史项管理

标准定义

关于历史的管理,和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 onl oad 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=http://www.mamicode.com/"Goto next page" onclick="gotoNextPage();">
  <input type="button" value=http://www.mamicode.com/"Start repeating timer" onclick="startRepeatingTimer();">
  <p>
  <div id="Timer">Paused</div>
  </p>
  <script type="text/javascript">

  function gotoNextPage(){
    setTimeout(function(){location.href=http://www.mamicode.com/"http://www.webkit.org/";},300);
  }

  function startRepeatingTimer(){
    setInterval(function(){document.getElementById("Timer").innerHTML="Running...";},500);
  }
  </script>

  <img src=http://www.mamicode.com/"http://therecoveringpolitician.com/wp-content/uploads/2013/01/moderate.jpg" width="640" height="480">
  <img src=http://www.mamicode.com/"http://cullogo.com/full/wallpapers-high-resolution-widescreen-hd-pk.jpg" width="640" height="480">
  <img src=http://www.mamicode.com/"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&amp;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://www.mamicode.com/"http://m.baidu.com/?abc=23&t=20080225" id="sub_iframe" width="320" height="480"></iframe>

  </body>
</html>



WebKit的历史项管理