マイ備忘録

あくまで個人の意見、メモです。

メモ : History API

Reactを少しいじったときにRoutingでどうなっているだろうと気になって調べた。

React Routerは画面遷移(URLの書き換え)にHistory APIを利用している。

A <Router> that uses the HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URL.

github.com

historyというpackageでAPIをラップしているっぽい。

github.com

History APIはブラウザの履歴情報にアクセスするAPI

html.spec.whatwg.org

pushStateやreplaceStateの第一引数のstateは、履歴スタックに状態をpushする際に合わせて保存しておきたいものを入れるっぽい。 MDNにはシリアライズできるものなら何でもOKと書いてあるので、ほんとになんでも良さそう。

developer.mozilla.org

一応HTMLの仕様も見てみる。 HistoryのInterface。

enum ScrollRestoration { "auto", "manual" };

[Exposed=Window]
interface History {
  readonly attribute unsigned long length;
  attribute ScrollRestoration scrollRestoration;
  readonly attribute any state;
  undefined go(optional long delta = 0);
  undefined back();
  undefined forward();
  undefined pushState(any data, DOMString title, optional USVString? url = null);
  undefined replaceState(any data, DOMString title, optional USVString? url = null);
};

html.spec.whatwg.org

折角なのでChromiumの実装を見てみる。

blinkの機能のハズなので、このコードがそうだろう。 WebIDLのinterfaceも同じディレクトリにあるし。

void History::pushState(v8::Isolate* isolate,
                        const ScriptValue& data,
                        const String& title,
                        const String& url,
                        ExceptionState& exception_state) {
  WebFrameLoadType load_type = WebFrameLoadType::kStandard;
  // Navigations in portal contexts do not create back/forward entries.
  if (DomWindow() && DomWindow()->GetFrame()->GetPage()->InsidePortal()) {
    DomWindow()->AddConsoleMessage(
        MakeGarbageCollected<ConsoleMessage>(
            mojom::ConsoleMessageSource::kJavaScript,
            mojom::ConsoleMessageLevel::kWarning,
            "Use of history.pushState in a portal context "
            "is treated as history.replaceState."),
        /* discard_duplicates */ true);
    load_type = WebFrameLoadType::kReplaceCurrentItem;
  }

  scoped_refptr<SerializedScriptValue> serialized_data =
      SerializedScriptValue::Serialize(isolate, data.V8Value(),
                                       SerializedScriptValue::SerializeOptions(
                                           SerializedScriptValue::kForStorage),
                                       exception_state);
  if (exception_state.HadException())
    return;

  StateObjectAdded(std::move(serialized_data), title, url,
                   ScrollRestorationInternal(), load_type, exception_state);
}

https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/core/frame/history.cc

第一引数のstateはほんとにシリアライズだけ行われているよう。 StateObjectAddedを読んでいくと、DocumentLoader::UpdateForSameDocumentNavigationあたりで履歴スタックを保存しているっぽい。

pushした履歴スタックの復元はHistory::goを見ればわかるかな。

void History::go(ScriptState* script_state,
                 int delta,
                 ExceptionState& exception_state) {
  if (!DomWindow()) {
    exception_state.ThrowSecurityError(
        "May not use a History object associated with a Document that is not "
        "fully active");
    return;
  }

  DCHECK(IsMainThread());
  auto* active_window = LocalDOMWindow::From(script_state);
  if (!active_window)
    return;

  if (!active_window->GetFrame() ||
      !active_window->GetFrame()->CanNavigate(*DomWindow()->GetFrame()) ||
      !active_window->GetFrame()->IsNavigationAllowed() ||
      !DomWindow()->GetFrame()->IsNavigationAllowed()) {
    return;
  }

  if (!DomWindow()->GetFrame()->navigation_rate_limiter().CanProceed())
    return;

  if (delta) {
    if (DomWindow()->GetFrame()->Client()->NavigateBackForward(delta)) {
      if (Page* page = DomWindow()->GetFrame()->GetPage())
        page->HistoryNavigationVirtualTimePauser().PauseVirtualTime();
    }
  } else {
    // We intentionally call reload() for the current frame if delta is zero.
    // Otherwise, navigation happens on the root frame.
    // This behavior is designed in the following spec.
    // https://html.spec.whatwg.org/C/#dom-history-go
    DomWindow()->GetFrame()->Reload(WebFrameLoadType::kReload);
  }
}

細かいところは全然わからないが、移動したいページの位置(delta)?があればページを復元して、なければリロードしているっぽい。 復元がされたらpopstateイベントが発生するはずだけど、そのあたりは読めていない。。